ドラゴンボールファイターズの光表現カッコいいから真似してみた

**▼普通のとき**
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/4c7d625c-d2d2-907c-6e85-6874bdd9f826.png">

**▼かめはめ波撃ったとき**
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/5bd7de3f-15ca-5b73-dacc-cfd09cacf25d.png">


**▼爆発したとき**
<img width="300" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/2c779c62-3658-ac30-0b6a-88b82165eac6.png">


いちいち光の色がアニメっぽくモデルに反映されて超カッコいいから真似したい。
けど何も考えずに真似すると **エフェクトの数だけLightを置かないといけなくて、負荷がシャレにならない。**
というわけで1ライトで表現するにはどうすべきか考えてみた。

# やってみた

こうなった。いい感じ？
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">シーン上のLightを動かさず・増やさずエフェクトの光を受ける機能実験。 <a href="https://t.co/KFFbiT0NPo">pic.twitter.com/KFFbiT0NPo</a></p>&mdash; 徳川バガ寅 (@flankids) <a href="https://twitter.com/flankids/status/1165267944227557381?ref_src=twsrc%5Etfw">August 24, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

## 今回デモ用に使ったAsset

* 言わずと知れた[ユニティちゃんトゥーンシェーダー2.0](http://unity-chan.com/download/releaseNote.php?id=UTS2_0)
  * デモ：https://www.youtube.com/watch?v=z7Pr39NW5Dk
* [Dwarf Pack](https://assetstore.unity.com/packages/3d/characters/humanoids/dwarf-pack-44913)
  * デモ：https://youtu.be/-wzWy-6JikU?t=53
  * このドワーフめっちゃ好き

# 考え方

* シェーダーに光の方向を直接入れるパラメータを追加
* 光の範囲、色を持つ **なんちゃってライト** をシーンに配置
* **なんちゃってライト** の範囲内にモデルが入ったら色・角度を設定して見た目に反映

##### TIPS：落ち影は無視

ここまで見てピンと来るかもですが、メッシュが受ける光の色と角度を改ざんするだけなので、**落ち影の角度は変わりません。**
(違和感は出るけど、Light増やさずリッチな光表現できるので許して！）
ドラゴンボールファイターズみたいにエフェクトの光を受けるような一時的な演出として使うと良さげ。

# 作り方

### モデル用意

まず好きなモデルに **ユニティちゃんトゥーンシェーダー**（以下UCTシェーダー）を当てて好きなパラメータを反映。

### UCTシェーダーのパラメータを拡張

* 光の角度を直接指定するか否か
* 光のベクトル（x, y, z）

のパラメータを追加してシェーダーに反映します。
編集するファイルは３つです。

* **Toon_DoubleShadeWirhFeather.shader**：シェーダーの外部用パラメータの定義場所
* **UCTS_DoubleShadeWirhFeather.cginc**：シェーダーの実際の計算をしているファイル
* **UCTS2GUI.cs**：Inspector上の表示を作る処理

```Toon_DoubleShadeWirhFeather.shader
// 150行目～
[Toggle(_)] _Inverse_Z_Axis_BLD (" Inverse Z-Axis (Built-in Light Direction)", Float ) = 1 // ←既存行
[Toggle(_)] _Is_MLD ("Advanced : Activate Manual Light Direction", Float ) = 0
_X_MLD (" X (Manual Light Direction)", Range(-1, 1)) = 0
_Y_MLD (" Y (Manual Light Direction)", Range(-1, 1)) = 0
_Z_MLD (" Z (Manual Light Direction)", Range(-1, 1)) = 1
```

```UCTS_DoubleShadeWirhFeather.cginc
// 111行目～
uniform fixed _Inverse_Z_Axis_BLD; // ←既存行
// マニュアルのライト角度
uniform fixed _Is_MLD;
uniform float _X_MLD;
uniform float _Y_MLD;
uniform float _Z_MLD;

～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～

// 226行目～
lightDirection = lerp(lightDirection, customLightDirection, _Is_BLD);
// マニュアルのライト角度
float3 munualLightDirection = normalize(float3(_X_MLD,_Y_MLD,_Z_MLD));
lightDirection = lerp(lightDirection, munualLightDirection, _Is_MLD);
```

```UCTS2GUI.cs
// 147行目～
MaterialProperty offset_Y_Axis_BLD = null; // ←既存行
MaterialProperty manualLightDirectionX = null;
MaterialProperty manualLightDirectionY = null;
MaterialProperty manualLightDirectionZ = null;

～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～

// 239行目～
offset_Y_Axis_BLD = FindProperty("_Offset_Y_Axis_BLD", props); // ←既存行
manualLightDirectionX = FindProperty("_X_MLD", props);
manualLightDirectionY = FindProperty("_Y_MLD", props);
manualLightDirectionZ = FindProperty("_Z_MLD", props);

～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～～

// 1945行目～
    EditorGUI.indentLevel--; // ←既存行
} // ←既存行

EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Manual Light Direction");
//GUILayout.Space(60);
if(material.GetFloat("_Is_MLD") == 0){
    if (GUILayout.Button("Off",shortButtonStyle))
    {
        material.SetFloat("_Is_MLD",1);
    }
}else{
    if (GUILayout.Button("Active",shortButtonStyle))
    {
        material.SetFloat("_Is_MLD",0);
    }
}
EditorGUILayout.EndHorizontal();
if(material.GetFloat("_Is_MLD") == 1){
    GUILayout.Label("    Manual Light Direction Settings");
    EditorGUI.indentLevel++;
    m_MaterialEditor.RangeProperty(manualLightDirectionX, "● Manual X Direction");
    m_MaterialEditor.RangeProperty(manualLightDirectionY, "● Manual Y Direction");
    m_MaterialEditor.RangeProperty(manualLightDirectionZ, "● Manual Z Direction");
    EditorGUI.indentLevel--;
}
```

編集後、さきほどシェーダーを設定したモデルの表示が破綻していないか確認しておきましょう。
見た目がピンク色になったり真っ黒になったりしてたら、編集ミスがあるので見直し！

### **なんちゃって光** と光を受けるモデル用のスクリプトを追加

* UTCシェーダーを持つモデルに作用させる光情報を持った **CellLookLight.cs** 
* *CellLookLight* の影響をUTCシェーダーモデルに反映させる **CellLookLightReceiver.cs** 

を追加します。

```CellLookLight.cs
using UnityEngine;

public class CellLookLight : MonoBehaviour {
	public Color Color {
		get {
			return _color;
		}
		private set {
			_color = value;
		}
	}
	[SerializeField]
	private Color _color = Color.white;

	public float Range {
		get {
			return _range;
		}
		private set {
			_range = value;
		}
	}
	[SerializeField]
	private float _range = 10f;

	private void OnDrawGizmos() {
		Gizmos.color = Color;
		Gizmos.DrawWireSphere(transform.position, Range);
	}
}
```

*OnDrawGizmos* の処理で光の色と範囲がシーンビュー上で可視化されるようになっています。

```CellLookLightReceiver.cs
using UnityEngine;

public class CellLookLightReceiver : MonoBehaviour {
	#region 定数
	// UCTシェーダーの各種パラメータ
	private const string PARAM_NAME_BASE_COLOR = "_BaseColor";
	private const string PARAM_NAME_1ST_SHADOW_COLOR = "_1st_ShadeColor";
	private const string PARAM_NAME_IS_MLD = "_Is_MLD";
	private const string PARAM_NAME_X_MLD = "_X_MLD";
	private const string PARAM_NAME_Y_MLD = "_Y_MLD";
	private const string PARAM_NAME_Z_MLD = "_Z_MLD";
	/// <summary>
	/// 光に当たったときどのくらい影を暗くするかのパラメータ（0～1）
	/// 値が大きいほど光に当たったときに明るいところと暗いところの差が強くなる
	/// </summary>
	private const float SHADE_DARKNESS_WEIGHT = 0.4f;
	#endregion

	#region 変数
	[SerializeField]
	private Renderer[] _renderers;
	// デフォルトのテクスチャカラー
	private Color[] _defaultBaseColors;
	private Color[] _default1stShadowColors;
	// CellLookLightPointによって受ける光情報
	private Color _addColor = Color.black;
	private bool _isManualLightDir = false;
	private Vector3 _manualLightDir = Vector3.up;
	#endregion

	private void Awake() {
		_defaultBaseColors = new Color[_renderers.Length];
		_default1stShadowColors = new Color[_renderers.Length];
		for (int i = 0; i < _renderers.Length; i++) {
			_defaultBaseColors[i] = _renderers[i].material.GetColor(PARAM_NAME_BASE_COLOR);
			_default1stShadowColors[i] = _renderers[i].material.GetColor(PARAM_NAME_1ST_SHADOW_COLOR);
		}
	}

	private void Update() {
		CellLookLight[] lights = FindObjectsOfType<CellLookLight>();
		float nearestDistanceRate = 1;
		Vector3 position = transform.position;
		CellLookLight targetLight = null;
		for (int i = 0; i < lights.Length; i++) {
			CellLookLight light = lights[i];
			float distanceRate = Vector3.Distance(light.transform.position, position) / light.Range;
			if (distanceRate >= nearestDistanceRate) {
				continue;
			}
			nearestDistanceRate = distanceRate;
			targetLight = light;
		}
		if (targetLight == null) {
			ResetLightInfo();
			return;
		}
		SetActiveManualLightDirection(true);
		SetAddColor(targetLight.Color);
		Vector3 diraction = (targetLight.transform.position - transform.position).normalized;
		SetManualLightDirection(diraction);
	}

	private void SetAddColor(Color addColor) {
		if (_addColor == addColor) {
			return;
		}
		_addColor = addColor;
		// 光が強いほど影に当たる部分は暗くなる
		Color shadowMultiColor = new Color(
			r: 1f - (_addColor.g + _addColor.b) * SHADE_DARKNESS_WEIGHT,
			g: 1f - (_addColor.r + _addColor.b) * SHADE_DARKNESS_WEIGHT,
			b: 1f - (_addColor.r + _addColor.g) * SHADE_DARKNESS_WEIGHT
		);
		for (int i = 0; i < _renderers.Length; i++) {
			_renderers[i].material.SetColor(PARAM_NAME_BASE_COLOR, _defaultBaseColors[i] + _addColor);
			_renderers[i].material.SetColor(PARAM_NAME_1ST_SHADOW_COLOR, _default1stShadowColors[i] * shadowMultiColor);
		}
	}

	private void SetManualLightDirection(Vector3 dir) {
		if (_manualLightDir == dir) {
			return;
		}
		_manualLightDir = dir;
		for (int i = 0; i < _renderers.Length; i++) {
			_renderers[i].material.SetFloat(PARAM_NAME_X_MLD, dir.x);
			_renderers[i].material.SetFloat(PARAM_NAME_Y_MLD, dir.y);
			_renderers[i].material.SetFloat(PARAM_NAME_Z_MLD, dir.z);
		}
	}

	private void SetActiveManualLightDirection(bool isActive) {
		if (_isManualLightDir == isActive) {
			return;
		}
		_isManualLightDir = isActive;
		for (int i = 0; i < _renderers.Length; i++) {
			_renderers[i].material.SetFloat(PARAM_NAME_IS_MLD, isActive ? 1 : 0);
		}
	}

	private void ResetLightInfo() {
		SetAddColor(Color.black);
		SetActiveManualLightDirection(false);
	}
}
```

Updateの中でシーン内にある *CellLookLight* を探し、最も影響が強い※ライトの色と角度をマテリアルに反映するようにしています。
※影響が強い＝光の範囲ギリギリの位置を１, 光と同じ位置を０とする距離割合の値が最も小さいもの

##### TIPS：陰の色に一工夫

**SetAddColor** メソッドでは、*BaseColor* にはシンプルに色を加算しているのに対して、*_1st_ShadeColor* に与える色はちょっとややこしいことをしています。
結論からいうと、 *BaseColor* が明るくなるほど、影である *1stShadow* は暗くなるようになっています。（ドラゴンボールファイターズリスペクト）
定数 *SHADE_DARKNESS_WEIGHT* の値でその程度が変えられるようになっているので、お好みで。


# 使い方

### 光を与えるUTCシェーダーのモデルに *CellLookLightReciver* コンポーネントをアタッチ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/80cf73cb-3e07-f5a8-2628-fd46574b4a3a.png)

Inspector内で、光の影響を与えたいモデル内のRencererオブジェクトを **Renderers** に参照を与えておいてください！

### 発光源として空のオブジェクトに *CellLookLight* コンポーネントをアタッチ＆配置

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/19b216e5-5d48-1b09-b004-6bf24029ec2d.png)

Inspector内で、色（Color）と範囲（Range）を設定。シーンビューに表示される球のギズモを参考に。

### 実行！

シーンを実行して、モデルを光の範囲内に入れたり範囲内でグルグル動かしたりしてみましょう。
光源の色の影響を受けたり、光源との位置関係によって陰影が変化したりする様子が確認できます。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">光の影響のデモ <a href="https://t.co/mptCOnrhmj">pic.twitter.com/mptCOnrhmj</a></p>&mdash; 徳川バガ寅 (@flankids) <a href="https://twitter.com/flankids/status/1165277076405936130?ref_src=twsrc%5Etfw">August 24, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# まとめ

メッシュに与える光の角度と色を変えることでゲーム中に光が発生したっぽい表現を実現できました。
残り課題として…

* 範囲内・外を出たり入ったりしたときにパカパカ色が変わる
* 光源との距離で光の影響具合が変わらない
* 複数の光源があってもどれか一つの影響しか受けない

などがありますが、いずれも既存メソッド内の計算を調整すれば対応できそうです。
また、Bloomを使えばよりそれっぽくなりそうなので、いろいろ応用が効きそう。

今回はここまで！

# 追記

## 2019/08/26

ストックしてくださった方が何人かいらっしゃったので、

> * 範囲内・外を出たり入ったりしたときにパカパカ色が変わる
> * 光源との距離で光の影響具合が変わらない

 ↑ を改善したデモを作ってみました。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">セルルック用のなんちゃってライトをちょっと拡張。範囲への入り・出でのパカパカをなくす対応 <a href="https://t.co/DK1wnbB7B5">pic.twitter.com/DK1wnbB7B5</a></p>&mdash; 徳川バガ寅 (@flankids) <a href="https://twitter.com/flankids/status/1165671247641858048?ref_src=twsrc%5Etfw">August 25, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

**CellLookLightReceiver.cs** の *Update* 内で計算してる`distanceRate`が１の時は光の影響が０、`distanceRate`が０の時は光の影響が１となるようにしています。

【ポイント】
**_Is_MLD**はデフォルトの光の角度と入力した光の角度をLerpで補完する割合の値としてシェーダー内で使っているので、1/0の間の値をdistanceRate由来の値にすることで急な影の角度の変化を防いでいます