[Unity] 実用的な雪の描画

[Unity 2 Advent Calendar 2015](http://qiita.com/advent-calendar/2015/unity2) 23日目の記事です。

# 軽い雪を！

Unity で雪を降らせましょう。それもデモデモしたカッコイイのじゃなくて、そのままゲームに使えちゃうような、 __なるべく実用的__なものを。つまり軽いやつ。
サンタさんありがとう的な雪を！

# GPU で描く

というわけで GPU を使うしかないですね。とはいえコンピュートとかは使いません。大抵のプラットフォームで動作するものを目指します。予め頂点を山ほど用意しておいて、頂点シェーダでビルボードにする作戦です。（まあ、よくある話なんですけど、意外とまとまったのはないような）

# Snow.cs
いきなりソースコード。上から順番に解説してきます。

```C#:Snow.cs
[RequireComponent(typeof(MeshFilter),typeof(MeshRenderer))]
public class Snow : MonoBehaviour
{
	const int SNOW_NUM = 16000;
	private Vector3[] vertices_;
	private int[] triangles_;
	private Color[] colors_;
	private Vector2[] uvs_;
```
↑クラスを用意し、頂点その他を用意します。Unity は１ドローコールで 65000 頂点まで扱えるので、一粒４頂点の雪はその 1/4 まで出せます。とりあえずめいいっぱいの 16000 個にしときましょう。GPU が悲鳴をあげたら減らせばいいのです。

```C#:Snow.cs続き
	private float range_;
	private float rangeR_;
	private Vector3 move_ = Vector3.zero;
```

↑その他のパラメータです。range_ は、雪を降らせる範囲を指定します。遠くまで描いても意味ないですからね。手前だけで十分。

```C#:Snow.cs続き
	void Start ()
	{
		range_ = 16f;
		rangeR_ = 1.0f/range_;
		vertices_ = new Vector3[SNOW_NUM*4];
		for (var i = 0; i < SNOW_NUM; ++i) {
			float x = Random.Range (-range_, range_);
			float y = Random.Range (-range_, range_);
			float z = Random.Range (-range_, range_);
			var point = new Vector3(x, y, z);
			vertices_ [i*4+0] = point;
			vertices_ [i*4+1] = point;
			vertices_ [i*4+2] = point;
			vertices_ [i*4+3] = point;
		}
```
↑ここから Start 関数です。範囲内のランダムな点に４頂点をまとめます。

```C#:Snow.cs続き
		triangles_ = new int[SNOW_NUM * 6];
		for (int i = 0; i < SNOW_NUM; ++i) {
			triangles_[i*6+0] = i*4+0;
			triangles_[i*6+1] = i*4+1;
			triangles_[i*6+2] = i*4+2;
			triangles_[i*6+3] = i*4+2;
			triangles_[i*6+4] = i*4+1;
			triangles_[i*6+5] = i*4+3;
		}
```
↑上の４頂点を矩形にするように結んで面にします。三角形ふたつずつですね。

```C#:Snow.cs続き
		uvs_ = new Vector2[SNOW_NUM*4];
		for (var i = 0; i < SNOW_NUM; ++i) {
			uvs_ [i*4+0] = new Vector2 (0f, 0f);
			uvs_ [i*4+1] = new Vector2 (1f, 0f);
			uvs_ [i*4+2] = new Vector2 (0f, 1f);
			uvs_ [i*4+3] = new Vector2 (1f, 1f);
		}
```
↑UV座標です。テクスチャを張るので当然必要なのですが、今回はテクスチャ座標以外にも使用します。この後のシェーダ編で触れます。

```C#:Snow.cs続き
		Mesh mesh = new Mesh ();
		mesh.name = "MeshSnowFlakes";
		mesh.vertices = vertices_;
		mesh.triangles = triangles_;
		mesh.colors = colors_;
		mesh.uv = uvs_;
		mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 99999999);
		var mf = GetComponent<MeshFilter> ();
		mf.sharedMesh = mesh;
	}
```
↑メッシュに格納します。bounds にすごい値を入れているのは、Frustom Culling されないため。Unity は Frustom Culling を無効にできないんです。
ここまでが Start 関数でした。かなり重い処理なので、間違っても Update に入れないように。

```C#:Snow.cs続き、最後
	void LateUpdate ()
	{
		var target_position = Camera.main.transform.TransformPoint(Vector3.forward * range_);
		var mr = GetComponent<Renderer> ();
		mr.material.SetFloat("_Range", range_);
		mr.material.SetFloat("_RangeR", rangeR_);
		mr.material.SetFloat("_Size", 0.1f);
		mr.material.SetVector("_MoveTotal", move_);
		mr.material.SetVector("_CamUp", Camera.main.transform.up);
		mr.material.SetVector("_TargetPosition", target_position);
	}
}
```
↑そして毎フレームの更新です。transform の更新後に呼ばれて欲しいので、LateUpdate にしました。内容はほとんどシェーダ定数の設定だけですね。target_position は、カメラからの前方位置を計算しておき、ここを中心に雪が降ります。

* _Range : 雪の範囲
* _RangeR : _Range の逆数（計算効率のため）
* _Size : 雪の粒の大きさ
* _MoveTotal : 移動量
* _CamUp : カメラの視線ベクトルに対して９０度上のベクトル
* _TargetPosition : 雪の範囲の中心

こんな感じ。次にシェーダ。

# snow.shader

```C++:snow.shader
Shader "Custom/snow" {
    Properties {
		_MainTex ("Base (RGB)", 2D) = "white" {}
    }
```
↑テクスチャを使うよと。

```C++:snow.shader続き
	SubShader {
   		Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
		ZWrite Off
		Cull Off
		Blend SrcAlpha OneMinusSrcAlpha // alpha blending
```
↑透明のパスに描きます。あとは Z は書かない、backface culling しない、アルファブレンドをミックスで。

```C++:snow.shader続き
        Pass {
 			CGPROGRAM

 			#pragma vertex vert
 			#pragma fragment frag
 			#pragma target 3.0
 			
 			#include "UnityCG.cginc"

            uniform sampler2D _MainTex;

 			struct appdata_custom {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
			};

 			struct v2f {
 				float4 pos:SV_POSITION;
				float2 uv:TEXCOORD0;
 			};
 			
 			float4x4 _PrevInvMatrix;
			float3   _TargetPosition;
			float    _Range;
			float    _RangeR;
			float    _Size;
			float3   _MoveTotal;
			float3   _CamUp;
```
↑ずらずらと決まり文句を書いて、

```C++:snow.shader続き
            v2f vert(appdata_custom v)
            {
				float3 target = _TargetPosition;
				float3 trip;
				float3 mv = v.vertex.xyz;
				mv += _MoveTotal;
				trip = floor( ((target - mv)*_RangeR + 1) * 0.5 );
				trip *= (_Range * 2);
				mv += trip;
```
↑ここから頂点シェーダ。今回のメインです。常に _TargetPosition の周りに登場するような計算をします。単に座標を足すのではダメで、うまくリピートさせないといけません。
`floor( ((target - mv)*_RangeR + 1) * 0.5 );`
これがキモですね。式の意味を理解したい方は[こちら](http://qiita.com/yuji_yasuhara/items/1b4d5f0052f074915f47#%E9%A0%82%E7%82%B9%E3%81%AE%E3%83%AA%E3%83%94%E3%83%BC%E3%83%88%E5%87%A6%E7%90%86)をどうぞ。

```C++:snow.shader続き
				float3 diff = _CamUp * _Size;
				float3 finalposition;
				float3 tv0 = mv;
				{
					float3 eyeVector = ObjSpaceViewDir(float4(tv0, 0));
					float3 sideVector = normalize(cross(eyeVector,diff));
					tv0 += (v.texcoord.x-0.5f)*sideVector * _Size;
					tv0 += (v.texcoord.y-0.5f)*diff;
					finalposition = tv0;
				}
```
↑ここで視線ベクトルを使ってビルボードにします。texcoord を利用して四隅を識別します。うまい具合に0と1が入っているので成り立つんですよね。なので UV には 0 と 1 以外の値を入れてはいけないのです。

```C++:snow.shader続き
            	v2f o;
			    o.pos = mul( UNITY_MATRIX_MVP, float4(finalposition,1));
				o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
            	return o;
            }
```
↑最後は普通に座標を出して完了。

```C++:snow.shader続き、最後

            fixed4 frag(v2f i) : SV_Target
            {
				return tex2D(_MainTex, i.uv);
            }

            ENDCG
        }
    }
}
```

↑フラグメントシェーダはテクスチャ引くだけ。

#画像を用意する
いきなり python。

```Python:image.py
import Image
import ImageDraw
import math

def drawing(img):
    x,y = img.size
    cx = x/2
    cy = y/2
    if x < y:
        rad = x/2
    else:
        rad = y/2
    draw = ImageDraw.Draw(img)
    for i in range(0,x):
        for j in range(0,y):
            dist = math.sqrt((i - cx)*(i - cx) + (j - cy)*(j - cy))
            val = 0xff*(1.0 - (dist/rad));
            draw.point((i, j), (0xff,0xff,0xff,int(val)))
    return img

def make_image(screen, bgcolor, filename):
    img = Image.new('RGBA', screen, bgcolor)
    img = drawing(img)
    img.save(filename)

if __name__ == '__main__':
    screen = (64,64)

    bgcolor=(0x00,0x00,0x00,0x00)

    filename = "snow.png"

    make_image(screen, bgcolor, filename)

#EOF
```

丸いテクスチャぐらいは、自力で作れるようになっときたいですね。特に alpha を厳密に指定したい場合は、プログラムで作っちゃったほうが早いです。断言。
ま、python の PIL という便利なものがあればこそですが。

これを実行すると、全面が white で alpha だけが中央からの距離減衰した画像が出来上がります。
![snow.png](https://qiita-image-store.s3.amazonaws.com/0/90046/d87a1e50-839b-0c31-80c0-317465bbf006.png "snow.png")
↑こんな感じ。

#実行する

さあ実行。もちろんシーンのセットアップが必要ですけど、そこは省略。まあ置くだけですよね。

![snow.png](https://qiita-image-store.s3.amazonaws.com/0/90046/c01208dc-092e-3f92-5df4-a71376da21ad.png "スクリーンショット 2015-12-21 22.31.09.png")

出ましたー！

![stats.png](https://qiita-image-store.s3.amazonaws.com/0/90046/1f9de193-15c8-b44a-32a2-bd4d59f03769.png "スクリーンショット 2015-12-21 22.31.25.png")

Batches: 7 と出てますが、再生を停止すると 6 でした。なので雪の処理単位は１です。やったね！

![scene.png](https://qiita-image-store.s3.amazonaws.com/0/90046/2f2ceeff-f8fd-632d-3a65-7259b3531362.png "スクリーンショット 2015-12-21 22.31.55.png")

ちゃんと、カメラの前にカタマリが来ています。カメラがどこにいても、その前にだけ降っているわけですね。

#降らす
降ってないのでは雪とは言えません。下に落下させましょう。頂点シェーダの実装を理解して移動させる必要があります。

```C#:Snow.cs(LateUpdate)
	void LateUpdate ()
	{
		var target_position = Camera.main.transform.TransformPoint(Vector3.forward * range_);
		var mr = GetComponent<Renderer> ();
		mr.material.SetFloat("_Range", range_);
		mr.material.SetFloat("_RangeR", rangeR_);
		mr.material.SetFloat("_Size", 0.1f);
		mr.material.SetVector("_MoveTotal", move_);
		mr.material.SetVector("_CamUp", Camera.main.transform.up);
		mr.material.SetVector("_TargetPosition", target_position);
		float x = 0f;
		float y = -2f;
		float z = 0f;
		move_ += new Vector3(x, y, z) * Time.deltaTime;
		move_.x = Mathf.Repeat(move_.x, range_ * 2f);
		move_.y = Mathf.Repeat(move_.y, range_ * 2f);
		move_.z = Mathf.Repeat(move_.z, range_ * 2f);
	}
```
↑LateUpdate の最後に移動を追加しました。こんな具合で _MoveTotal に入れる移動量を制御します。落下速度は `float y = -2f;` なので、秒速２メートルですね。移動したら Mathf.Repeat を使って循環させておきます。でないと数値が永遠に大きくなって、オーバーフローを起こしてしまいます。

#揺らす

お次は雪っぽく、フワフワさせましょう。今度は頂点シェーダに手を加えます。

```C++:snow.shader(vert)
            v2f vert(appdata_custom v)
            {
				float3 target = _TargetPosition;
				float3 trip;
				float3 mv = v.vertex.xyz;
				mv += _MoveTotal;
				trip = floor( ((target - mv)*_RangeR + 1) * 0.5 );
				trip *= (_Range * 2);
				mv += trip;

				float3 diff = _CamUp * _Size;
				float3 finalposition;
				float3 tv0 = mv;
				tv0.x += sin(mv.x*0.2) * sin(mv.y*0.3) * sin(mv.x*0.9) * sin(mv.y*0.8);
				tv0.z += sin(mv.x*0.1) * sin(mv.y*0.2) * sin(mv.x*0.8) * sin(mv.y*1.2);
				{
					float3 eyeVector = ObjSpaceViewDir(float4(tv0, 0));
					float3 sideVector = normalize(cross(eyeVector,diff));
					tv0 += (v.texcoord.x-0.5f)*sideVector * _Size;
					tv0 += (v.texcoord.y-0.5f)*diff;
					finalposition = tv0;
				}
            	v2f o;
			    o.pos = mul( UNITY_MATRIX_MVP, float4(finalposition,1));
				o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
            	return o;
            }
```
↑sin を重ね合わせる２行を追加しました。この雪はシェーダで作っているので、CPUで何かするときのようにオブジェクトの概念では動かせません。「場」の概念で揺らします。つまり空間を曲げる的な。
数字は適当で、周期をずらして重ねます。位相を縦軸にしたり横軸にしたりするのも工夫ですね。調整していると、楽しくていくらでも時間が過ぎていきます。

#最後にもうひと工夫
```C#:Snow.cs(LateUpdate)
	void LateUpdate ()
	{
		var target_position = Camera.main.transform.TransformPoint(Vector3.forward * range_);
		var mr = GetComponent<Renderer> ();
		mr.material.SetFloat("_Range", range_);
		mr.material.SetFloat("_RangeR", rangeR_);
		mr.material.SetFloat("_Size", 0.1f);
		mr.material.SetVector("_MoveTotal", move_);
		mr.material.SetVector("_CamUp", Camera.main.transform.up);
		mr.material.SetVector("_TargetPosition", target_position);
		float x = (Mathf.PerlinNoise(0f, Time.time*0.1f)-0.5f) * 10f;
		float y = -2f;
		float z = (Mathf.PerlinNoise(Time.time*0.1f, 0f)-0.5f) * 10f;
		move_ += new Vector3(x, y, z) * Time.deltaTime;
		move_.x = Mathf.Repeat(move_.x, range_ * 2f);
		move_.y = Mathf.Repeat(move_.y, range_ * 2f);
		move_.z = Mathf.Repeat(move_.z, range_ * 2f);
	}
```

y で落下させるだけでなく、x と z にも値を入れます。ここは全体を動かす「風」になるので、PerlinNoise で揺らぎを入れてみましょう。PerlinNoise の入力は平面なのですが、今回はその縁だけ使わせてもらいます。

気まぐれな風が吹くようになりました。これにて完成とします。

#こうなりました

動画です。３０秒：https://vimeo.com/149642482

プロジェクト一式はこちらに置きました：
https://github.com/dsedb/snowflakes

#メリークリスマス！（早いけど）
ゲーム作る人に幸あれ。