UnityのImage Effectでマンガ風の画面を作ってみる

この記事は KLab Engineer Advent Calendar 2018 7日目の記事です。
Advent Calendar は去年に続き参加する mizusawa-k です。よろしくお願いします。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/f550cfc8-27f3-9b53-edf5-c34771a1a6ae.png)

※本記事は[ユニティちゃん](http://unity-chan.com/)のモデルを利用して執筆し、[ユニティちゃんライセンス条項](http://unity-chan.com/contents/license_jp/)の元に提供しています
© Unity Technologies Japan/UCL

# はじめに
今月頭に [Tokyo Demo Fest](http://tokyodemofest.jp/2018/) が行われました。
KLabのエンジニアでも何名か参加していて5名も入賞するという素晴らしい成果が出ていますが、
参加してきた話を私自身聞いていて

何か面白そうだな。普段はビルドとかUI実装やってるけど、
自分もシェーダーとか使って絵作りしてみたいんだよね。

と思っていたところ、ちょうどAdvent Calendarに参加する予定がありいいきっかけだったので[^注1]
今回は普段使っているUnityで、シェーダー始めグラフィックス周辺の技術の深堀がてら、
ちょっと変わった絵をリアルタイムで出せるようにしたいと思いました。
[^注1]: 元々考えていたネタはRGBとかCMYKとかHDRとかとか、デジタルで色を扱う時に知っておくべき知識の再確認的な内容でした:)

# なぜ、Image Effectでマンガ風の画面か
今回は本当に思いつきから始まったのですが、
**「3D画面がマンガみたいな線とベタ、トーンだけの見た目で動いたら面白そう」**
というところが取っ掛かりでした。

私自身、4年くらい前、前職でやっていた仕事で、
iOS端末上で動作するカメラ写真の線画抽出プログラムを [OpenCV](http://opencv.jp/) で作った経験があったのですが
その時に感じた
「（色彩、階調豊かな）見慣れた景色が白黒の線とベタ、トーンで置き換えられた時のインパクト」
がなかなか忘れがたく、面白いものがあったので、
今回は、同じようなことをUnityで実装するためにはどうしたら（手早く）作れるかなということを考えました。
マンガ風の画面を作りたいなと思ったのはそういう取っ掛かりです。

ImageEffectを使った実装にした理由はひとことで言えば、
**「Unity 5.4で簡単に実装するのに使えそうだから
（Unity社からアセットがソースコードが見える状態で提供されていて、シェーダー初心者でも取っ付きやすそうだから）」**
です。

参考までに、ImageEffectに決めた経緯はざっくり以下のようなかんじです。

- 使用するUnityのバージョンは5.4系のみ（会社のPCで、業務の片手間でやるため）
- 以前iOSで使った経験があるOpen CVを使うなら

    - Unity向けラッパーアセットが有料で出ているのでそれを使うかプラグインを書くかの二択[^注2]
    - 有料アセットはそこそこいいお値段するので今回はパスしておきたい
    - プラグインは実装とか確認が手間だと思うので今回はあまり使いたくない

- というか、今回はそもそもカメラとかシェーダーとか、3D周りの勉強したい
- 画面全体に効果をつけるならポストエフェクトというものを使うと良いらしい
- Unityでは5.4までのポストエフェクトにはImage Effectを提供している
- Unityが提供しているStandard Assetsの中にあるものでそのまま使えそうなImageEffectが結構ある
- 結論：何か良さそうだからImageEffect使ってみる

[^注2]: [OpenCV for Unity](https://assetstore.unity.com/packages/tools/integration/opencv-for-unity-21088) というラッパーアセットがアセットストアで公開されています


# マンガ風の画面とは

## マンガ風の画面のイメージ
以下のような、マンガ風写真加工アプリで作れる画像のような画面を想定しています。

<img src="https://qiita-image-store.s3.amazonaws.com/0/215740/a51bf163-e8b0-0fbf-0792-c96f6ddb62c3.jpeg" width=40%>　→　<img src="https://qiita-image-store.s3.amazonaws.com/0/215740/68dea2a4-82cd-febe-bd11-c7f3a86e4a91.jpeg" width=40%>


マンガ風写真加工アプリは以下のようなものがあるようです。

- [漫画コミックカメラ - 無料で写真をマンガ化。撮影した写真やカメラロールの画像をフィルターで線画加工、漫画風に画像加工](https://itunes.apple.com/jp/app/%E6%BC%AB%E7%94%BB%E3%82%B3%E3%83%9F%E3%83%83%E3%82%AF%E3%82%AB%E3%83%A1%E3%83%A9-%E7%84%A1%E6%96%99%E3%81%A7%E5%86%99%E7%9C%9F%E3%82%92%E3%83%9E%E3%83%B3%E3%82%AC%E5%8C%96-%E6%92%AE%E5%BD%B1%E3%81%97%E3%81%9F%E5%86%99%E7%9C%9F%E3%82%84%E3%82%AB%E3%83%A1%E3%83%A9%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AE%E7%94%BB%E5%83%8F%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%BC%E3%81%A7%E7%B7%9A%E7%94%BB%E5%8A%A0%E5%B7%A5-%E6%BC%AB%E7%94%BB%E9%A2%A8%E3%81%AB%E7%94%BB%E5%83%8F%E5%8A%A0%E5%B7%A5/id578506865)
- [漫画カメラ](https://itunes.apple.com/jp/app/%E6%BC%AB%E7%94%BB%E3%82%AB%E3%83%A1%E3%83%A9/id557883632?mt=8&ign-mpt=uo%3D4)

これらのアプリでは、写真を線画とベタ、トーンの画像に変換出来るほかに、
集中線を重ねてさらにマンガっぽく出来るようになっていたりします。なかなか面白いです。

※「マンガ風の画面のイメージ」で紹介した画像は、箇条書き一つ目の「漫画コミックカメラ」で作成しています。

## マンガ風の画面の要素
画面の要素としては以下です。

- 輪郭が細い線画で描かれている
- 明るい部分が白で描かれている
- 暗い部分が黒（ベタ）で描かれている
- 明るい部分と暗い部分の中間の部分がトーンで描かれている

今回は時間の都合上、最後のトーンの部分は割愛し、
簡単に出来そうな白と黒（ベタ）だけの画面を作って行きたいと思います。

# 実際に作ってみる
前置きが少し長くなりましたが、実際に作っていきます。

## 開発＆実行環境
- Unity 5.4.2 p2
- PC, Mac&Linux Standalone

## マンガ風の画面を作るまでの流れ
今回は、ImageEffectを使って色々と手を動かしていった結果、
最終的に得たい絵を出すまでの流れが以下のようになりました。

**1. 線画のみ抽出した絵をRenderTextureに書き出す
2. ベタのみ抽出した絵をRenderTextureに書き出す
3. 書き出した線画とベタのRenderTextureを一枚の絵としてまとめる
4. まとめた絵をメインカメラに映す**

RenderTextureというのは、カメラに映っている絵を
画面外の画像として描き出しておくことができるUnityの機能です。
RenderTextureを使う事で、例えば毎フレームの描画処理を軽減したり、
ライブシーンでよく見られるモニター画像をリアルタイムに描き出すような
表現の幅を広げたりすることができます。

今回は簡単に絵を出してみたかったので、撮りたい絵の位置などを決めた後、
必要なRenderTextureごとにカメラを複製してヒエラルキ上に配置しました。

## カメラに映っている絵をRenderTextureに描き出す方法
プロジェクト上にRenderTextureを新規作成してから、
描き出したいカメラをヒエラルキ上で選択して、カメラのインスペクタ上でターゲットにRenderTextureを設定します。

**ベタ用カメラ（Beta Camera）のTargetTextureにベタ用RenderTexture（BetaRenderTexture）を設定**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/5a1f6140-c196-df64-f739-3017cfcbcaff.png)

**線画用カメラ（Line Camera）のTargetTextureに線画用RenderTexture（LineRenderTexture）を設定**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/b59683e9-f0ee-6e81-5fe3-5f6831f4ce9b.png)


カメラのターゲットにRenderTextureを設定した後は、
プロジェクト上でRenderTextureを選択した際にインスペクタ上に表示される情報に
カメラからRenderTextureに書きこまれた絵が表示されるようになります。

**RenderTextureのインスペクタ**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/0b7c4e5b-a534-2b54-40ff-6f22ffceaa76.png)

RenderTextureの設定では描き出すテクスチャのサイズを設定できますが、
今回は16:9でそれなりの解像度で画面に出したかったので **1440 * 768** というサイズ設定で統一させました。

## カメラに映っている絵にImageEffectを適用する方法
カメラにImageEffectのコンポーネントをつけて、パラメータを調整するだけです。

UnityのStandard AssetsにはいろいろなImageEffectがありましたが、
今回私が使用したのは以下のImageEffectです。

**今回利用した Unity StandardAsset の ImageEffect**

| イメージエフェクトの名前 |                                使用用途                                 |
|:-----------------------|-----------------------------------------------------------------------:|
| EdgeDetection          |                           線画を抽出するイメージエフェクトとしてそのまま使用|
| Grayscale              |              ベタのイメージエフェクトを作成する際に流用（そのまま使用はせず）|
| ContrastStretch        |                                                        ベタの調整用として|

今回そのまま利用したのは EdgeDetection の ImageEffect のみですが、
この理由は線画抽出を手早く簡単に実現したかったためです。

ImageEffect のコンポーネントをつけたカメラは、たとえば線画なら以下のようになりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/cddb3467-231d-8cd4-e267-279b7546dcbe.png)

EdgeDetectionが複数ついているのは、片方だけだと髪の線画と顔周りの線画どちらかだけになってしまったからです。
両方同時に出すために、二つの線画抽出処理を実行させるようにしています。

カメラから得られた絵に対して線画のイメージエフェクトを適用した結果は以下です。

**線画イメージエフェクトの適用結果**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/c02e393f-6e2a-c310-f574-996617c3cc13.png)

↓

![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/e47174fe-c758-776e-01b4-a04b1d66229d.png)

## ベタのImageEffectの自作

ベタの絵を出すのにあたっては、
適切そうなImageEffectがStandardAssetsになかったので、自作を考えました。
とは言え、イチからイメージエフェクトやシェーダーを作るのもちょっと大変そうだったので、
流用してちょっと調整することが出来ないかと考えました。
StandardAssetsを眺めていたところ、ちょうどイメージエフェクトの中に
画面をグレイスケールに変換するというGrayScaleEffectというものがあったので
それを流用するようにしました。

基本的には影の部分など、特定の暗さの部分を陰のような形で出せればいいなと思ったので、
とりあえず輝度を見て、一定の範囲内の濃度なら指定された色として、範囲外なら透明にするようにしています。
指定色以外のピクセルを透明にするようにしたのは、後述する線画との合成するデカールシェーダーとの都合です。

ベタのImageEffectのコード（Unityのインスペクタ上で表示されるインターフェース部分）は以下です。

```csharp:ThresholdImageEffect.cs
using System;
using UnityEngine;
using UnityStandardAssets.ImageEffects;

namespace ComicShader.ImageEffects
{
    /// <summary>
    /// 濃度の上限と下限を指定して、指定した色でベタ塗りするイメージエフェクトです
    /// 指定範囲外の濃度の部分は透明な色になります
    /// </summary>
    [ExecuteInEditMode]
    [AddComponentMenu("CommicShader/ImageEffects/ThresholdImageEffect")]
    public class ThresholdImageEffect : ImageEffectBase {

        /// <summary>
        /// ベタで塗る色（注：濃度しか見られていないので彩度のある色指定は未対応）
        /// </summary>
        public Color    color;

        /// <summary>
        /// 上限値
        /// </summary>
        [Range(0.0f,1.0f)]
        public float    upperThresholdValue;

        /// <summary>
        /// 下限値
        /// </summary>
        [Range(0.0f,1.0f)]
        public float    lowerThresholdValue;

        // Called by camera to apply image effect
        void OnRenderImage (RenderTexture source, RenderTexture destination) {
            material.SetColor("_Color", color);
            material.SetFloat("_ThresholdUpper", upperThresholdValue);
            material.SetFloat("_ThresholdLower", lowerThresholdValue);
            Graphics.Blit (source, destination, material);
        }
    }
}
```
このインターフェースでこのようなインスペクタが表示されます。

**ベタのイメージエフェクトのインスペクタ表示**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/146237d9-0fea-0ab6-c3a2-0d4aa9092660.png)

ベタのImageEffectのシェーダーコード（実際にテクスチャの色を弄ってる部分）は以下です。

```glsl:Shader/ThresholdShader.shader

Shader "Shader/ThresholdShader" {
Properties {
	_MainTex ("Base (RGB)", 2D) = "white" {}
	_RampTex ("Base (RGB)", 2D) = "grayscaleRamp" {}

	// ThresholdShader追加分
	_Color ("Color", Color) = (1,1,1,1)
	_ThresholdUpper ("ThresholdUpper", Range(0.0,1.0)) = 0.0
	_ThresholdLower ("ThresholdLower", Range(0.0,1.0)) = 0.0
}

SubShader {
	Pass {
		ZTest Always Cull Off ZWrite Off
				
CGPROGRAM
#pragma vertex vert_img
#pragma fragment frag
#include "UnityCG.cginc"

uniform sampler2D _MainTex;
uniform sampler2D _RampTex;
//uniform half _RampOffset;		// 不要なのでコメントアウト
half4 _MainTex_ST;

// ThresholdShader追加分
uniform half _ThresholdUpper;
uniform half _ThresholdLower;
fixed4 _Color;


fixed4 frag (v2f_img i) : SV_Target
{
	fixed4 original = tex2D(_MainTex, UnityStereoScreenSpaceUVAdjust(i.uv, _MainTex_ST));
	fixed grayscale = Luminance(original.rgb);

	half2 remapC = 0;
	half2 remapA = 0;

	if (_ThresholdLower < grayscale && grayscale < _ThresholdUpper)
	{
		remapC = _Color;
		remapA = 1;
	}

	fixed4 output = tex2D(_RampTex, remapC);
	output.a = remapA;
	return output;
}
ENDCG

	}
}

Fallback off

}
```

カメラから得られた絵に対してベタのイメージエフェクトを適用した結果は以下です。

**ベタイメージエフェクトの適用結果**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/c02e393f-6e2a-c310-f574-996617c3cc13.png)

↓

![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/81a9de33-70cf-b49c-b30f-3e66730c79f4.png)

## 線画とベタを合成する
線画とベタを合成する部分は、
テクスチャの上に汚れやシミなどのテクスチャを重ねる際などに使用する
DecalというシェーダーがUnityのプリインストールに入っているらしい記事を見かけたので、
Decalシェーダーを使用して実装しました。

実装にあたっては、
**CommicMaterial** という名前のマテリアルをプロジェクト上に新たに作成して、
作成したCommicMaterialにDecalシェーダーと線画＆ベタのTextureをそれぞれ設定しています。

**線画とベタをまとめるCommicMaterialのインスペクタ表示**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/4c306fdf-1d49-8b26-430e-e6272617b786.png)

今回は

- **Base に線画の RenderTexture（LineRenderTexture）**
- **Decal にベタの RenderTexuture（BetaRenderTexuture）**

といったかたちでそれぞれ設定しました。

## 合成した絵をメインカメラで表示する
メインカメラでの描画にあたっては、
あまりうまい方法ではない気がしましたが簡単に絵を出せる方法として
uGUIのCanvasを作り、CanvasにCommicMaterialを設定して、
メインカメラからCanvasのみを映す形にしました。

ちょっとわかりにくいですが、シーンはこんなかんじになっています。

**シーンビューでCanvas, Cameraの配置を確認した表示**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/d41389ea-64a0-7100-edc9-d93f9b7d5b4c.png)


また、せっかくリアルタイムで動くシェーダーで描いているので、
最終的にメインカメラで描画される絵に動きをつけることを考えてみます。

キャラクター自身が身体を動かしたり表情を変えるようなアニメーションを付けるのも考えましたが、
今回はカメラの方をキャラクターを中心にくるくると回転移動させることにします。
（Canvasを映しているメインカメラではなく、線画とベタのRenderTextureを描き出しているカメラ二つの方）

キャラクターを中心に線画とベタそれぞれを描き出しているカメラを回転移動させるために、
線画とベタの ImageEffect がついたカメラを一つの GameObject の子としてまとめて、
二つのカメラをまとめた GameObject に以下のようなコンポーネントを付けます。

```csharp:Revolver.cs

using UnityEngine;
using System.Collections;

/// <summary>
/// 設定したターゲットを中心として、Y軸で回転し続けるスクリプト
/// </summary>
public class Revolver : MonoBehaviour {

    /// <summary>
    /// 一秒で回転させる角度
    /// </summary>
    public  float angle = 50f;

    /// <summary>
    /// 回転の中心となるターゲット
    /// </summary>
    [SerializeField]
    Transform target;

    void Update () {
        Vector3 targetPos = target.transform.position;
        Vector3 axis = target.TransformDirection(Vector3.up);
        
        // ターゲットを中心にY軸で毎秒angle分だけ回転させる
        transform.RotateAround(targetPos, axis, angle * Time.deltaTime);
    }
}
```

上記のコンポーネントを付けると、以下のような形で回転する角度とターゲットが設定できるようになっています。

**Revolver をコンポーネントとしてつけた GameObject のインスペクタ表示**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/165dfe2a-1908-a00d-3939-e6ad5c480018.png)


回転する角度は実行時にも書き換えられるので、いったんデフォルト値のままとしますが、
ターゲットはユニティちゃんを設定しておきます。

## 今回作ったマンガ風画面の表示結果
作成したマンガ風画面の実行結果は以下のようになります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/f550cfc8-27f3-9b53-edf5-c34771a1a6ae.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/d1b91212-74a8-94e3-e5a1-bb5fbdd856be.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/215740/14b9b0d4-6e81-c7b4-ebce-82450680158a.png)

Unityのエディタ上で動かしているシェーダーなので、
例えばベタのイメージエフェクトで閾値を変更すればベタの領域を広げる・狭くするということも出来ますし、
線画のイメージエフェクトでModeを変更すれば線画の微妙な出方を変更したり出来ます。

# 今後の改良点
今回はひとまず絵を出すことを目標として進めましたが、
今後より実用的なものにするためには

- トーンのシェーダーを追加する
- 線画、トーン、ベタ全てをマンガ風シェーダー（マテリアル）にまとめてパラメータでトーンやベタの出方を設定できるようにする
- 実機でのシェーダーの容量や速度面での最適化する

といった改良点が考えられると思います。

また、絵作り的な面では、線画抽出のシェーダーを自作するというのも面白いかもしれません。
このあたりの改善は、次の [技術書典](https://techbookfest.org/) の原稿あたりで出来たらいいな。と思います。[^注4]
[^注4]: 実はKLabのエンジニア有志で技術書典に何度か参加しています。前回の[技術書典5](https://techbookfest.org/event/tbf05)で参加した時の紹介記事は[こちら](http://klabgames.tech.blog.jp.klab.com/archives/tbf05pre.html)。技術書典5で頒布した本では、私はHoudiniのプロシージャルモデリング機能を紹介しました

# おわりに
ここまで、駆け足ですが Unity の ImageEffect を使ってマンガ風の画面を出す方法を紹介していきました。
私自身はシェーダーはちょっとかじったレベル（シェーダープログラムをいくつか写経したくらい）で、
ImageEffect も RenderTextureも初めて触りましたし、
自分が作りたい絵を出すためにシェーダーを書くのは今回が初めてでしたが、
業務の傍らググりながら2, 3日程度の期間で何とか形にする事ができました。

こうして実際に動くもの、絵が出来てくると、
来年の Tokyo Demo Fest も参加考えてみようかなという気持ちになってきます。
Tokyo Demo Fest、ちらっと見たところ、4K以内のファイルで動画をレンダリングするような
ストイックな部門だけではないようです。

せっかく色がある画面をモノクロにまで情報落とすのも何だかもったいないかんじもありますが、
色鮮やかな映像が溢れている3Dコンテンツの中でモノクロマンガ的な絵が動いていると
それだけで少し目新しいかんじを受けられるのではと思います。
マンガ系のIPコンテンツで、ここぞというところの演出で使うのも面白いと思います。

今後の改良点にまとめた通り、現状はまだ最初の一歩というような内容ではありますが、
Unity 5 ～ 5.4のバージョンで開発しているアプリなどでは参考に出来る情報もあるかと思います。
自分の備忘録がてらつらつらまとめたところもあり
内容の割に少々長い記事になってしまったという感も否めないですが、
本記事が読者の皆様の Unity での3Dゲーム作り、絵作りの一助となればうれしいです。

明日の Advent Calendar は [@hohean](https://qiita.com/hohean) さんです。お楽しみに。