#雨のシーン
をレンダリングする際、雨粒を垂直に伸びた線で表現する手法はよくみられるんですが、そもそも雨粒は丸い粒のはずで、シャッタースピードが長いから、それが線のように見えている。わけですよね?
なので、伸びる方向はカメラの位置・角度の変化で決まるべきかなと。モーションブラーっぽく。
つまり、たとえば、
こんな風に螺旋を描いて欲しいのです。と、いうような雨粒をレンダリングしてみます。
#作戦
- 実用性を念頭に入れて、シェーダで書きます
- 普通に Unity を使います。Unity 5.2.0f3 です
- POLYGON でなく LINE で書きます、今回は。
#頂点を準備する
using UnityEngine;
using System.Collections;
public class Debris : MonoBehaviour
{
const int POINT_MAX = 4096;
private Vector3[] vertices_;
private int[] indices_;
private Color[] colors_;
private Vector2[] uvs_;
private float range_;
private float rangeR_;
private float move_ = 0f;
private Matrix4x4 prev_view_matrix_;
void Start ()
{
range_ = 32f;
rangeR_ = 1.0f/range_;
vertices_ = new Vector3[POINT_MAX*3];
for (var i = 0; i < POINT_MAX; ++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*3+0] = point;
vertices_ [i*3+1] = point;
vertices_ [i*3+2] = point;
}
indices_ = new int[POINT_MAX*3];
for (var i = 0; i < POINT_MAX*3; ++i) {
indices_ [i] = i;
}
colors_ = new Color[POINT_MAX*3];
for (var i = 0; i < POINT_MAX; ++i) {
colors_ [i*3+0] = new Color (1f, 1f, 1f, 0f);
colors_ [i*3+1] = new Color (1f, 1f, 1f, 1f);
colors_ [i*3+2] = new Color (1f, 1f, 1f, 0f);
}
uvs_ = new Vector2[POINT_MAX*3];
for (var i = 0; i < POINT_MAX; ++i) {
uvs_ [i*3+0] = new Vector2 (1f, 0f);
uvs_ [i*3+1] = new Vector2 (1f, 0f);
uvs_ [i*3+2] = new Vector2 (0f, 1f);
}
Mesh mesh = new Mesh ();
mesh.name = "debris";
mesh.vertices = vertices_;
mesh.colors = colors_;
mesh.uv = uvs_;
mesh.bounds = new Bounds(Vector3.zero, Vector3.one * 99999999);
var mf = GetComponent<MeshFilter> ();
mf.sharedMesh = mesh;
mf.mesh.SetIndices (indices_, MeshTopology.Lines, 0);
prev_view_matrix_ = Camera.main.worldToCameraMatrix;
}
Start() で頂点を作っています。半径 radius_ の立方体に、ランダムで点を置きます。このとき同じ座標に、3つの点を置きます。伸ばすための座標をあらかじめ用意するわけです。
なぜ2つでなく3つかというと、ひとつの Mesh でグラデーションの線をたくさん書くには、不連続点を埋める「つなぎ」の座標が必要だったからです。以下の図で黒は描画したい雨粒、白は都合上描画してしまうので透明にしたい領域です。
3つの頂点に Color として 0, 1, 0 を与えます。1番目は不連続点つなぎ用(透明)、2番目は実際の雨粒の座標、3番目は尾を引いた軌跡座標(透明)として使われます。
さらに UV として (1, 0), (1, 0), (0, 1) を与えます。これはテクスチャを引くためではなく、シェーダへのヒントとして渡します。u が 1 であれば雨粒座標、v が 1 であれば軌跡座標です。3番目だけが軌跡座標となります。
そして MeshTopology は Lines に設定します。
あと、どうやら Unity さんは Frustom Culling をオフにできないらしいので、Bounds をでかい値に設定しておきます。かっちょ悪い。
#シェーダ定数を更新をする
上のコードの続きです。実際に軌跡を計算するのはシェーダになります。ここではヒントとなる情報を更新します。共通の処理を定数に入れて、頂点ごとの負荷を抑えます。
void Update ()
{
var target_position = Camera.main.transform.TransformPoint(Vector3.forward * range_);
var matrix = prev_view_matrix_ * Camera.main.cameraToWorldMatrix; // prev-view * inverted-cur-view
var mr = GetComponent<Renderer> ();
const float raindrop_speed = -1f;
mr.material.SetFloat ("_Range", range_);
mr.material.SetFloat ("_RangeR", rangeR_);
mr.material.SetFloat ("_MoveTotal", move_);
mr.material.SetFloat ("_Move", raindrop_speed);
mr.material.SetVector ("_TargetPosition", target_position);
mr.material.SetMatrix ("_PrevInvMatrix", matrix);
move_ += raindrop_speed;
move_ = Mathf.Repeat(move_, range_ * 2f);
prev_view_matrix_ = Camera.main.worldToCameraMatrix;
}
}
雨粒はカメラの前だけに存在すれば良いので
- _TargetPosition:カメラ前方の座標
を算出します。そして前のフレームのカメラ座標を設定するのですが、シェーダの計算上の都合で
- _PrevInvMatrix:前回のカメラ行列 x 現在のカメラ行列の逆行列
を算出しておきます。そのほか、
- _Range:雨粒の範囲
- _RangeR:雨粒の範囲の逆数
- _MoveTotal:トータルの雨粒の落下移動量
- _Move:フレーム単位の雨粒の落下移動量
を設定しておきます。
#頂点のリピート処理
改めて、頂点のリピート処理を考えます。カメラの前に座標を移動させて、まるで世界のすべてが覆われているかのように見せるテクニックです。xyz 軸で独立しているので、一次元の数直線で考えましょう。
まず、ランダムに打った点のひとつを v 、ターゲットの座標(カメラの前に置いた座標)を p とします。乱数に使用した範囲は -R から R で 2R になります。
v を 2R でリピートしていって、p の付近のどの位置になるかを知りたいのです。リピート結果を v' とします。
p の近くに二つの v' が現れました。p±R の範囲に入っているものが求める値です。
数式で表します。
$v' = v + 2nR$
この n を知りたいわけなので、
$v' < P+R$
より、
$n < 1/2((P-V)/R)+1)$
これを満たす最大の整数が求める n となります。シェーダでは3軸まとめて
float3 trip = floor( ((target - v.vertex.xyz)*_RangeR + 1) * 0.5 );
このように記述できます。(割り算は重そうなのであらかじめ逆数 _RangeR を計算しておきます)
#任意のカメラでの座標変換
軌跡部分を、指定した View で(ひとつ前のカメラで)座標変換します。
ただ、頂点には
- 先頭用
- 軌跡用
があるんですが、vertex シェーダにはどちらが来ているかわかりません。そこで両方を計算して、あらかじめ用意しておいた UV 座標にあるヒントで、片方にゼロをかけます。無駄なようですが、下手に分岐を書くよりもこの方が処理速度が出る気がしました。計測はしていませんが。
float4 tv0 = v.vertex * v.texcoord.x;
tv0 = mul (UNITY_MATRIX_MVP, tv0);
先頭用。基本的なマトリクス変換です。軌跡用の頂点が来た場合は v.texcoord.x がゼロになっているため、結果がゼロになります。
float4 tv1 = v.vertex * v.texcoord.y;
tv1 = mul (UNITY_MATRIX_MV, tv1);
tv1 = mul (_PrevInvMatrix, tv1);
tv1 = mul (UNITY_MATRIX_P, tv1);
軌跡用、すなわち前のフレームのカメラでの変換です。Model と View をかけておいて、_PrevInvMatrix をかけます。設定時に View の逆行列をかけておいたので、現在のカメラ行列はキャンセルされます。Projection をかけて、最後に
v2f o;
o.pos = tv0 + tv1;
二つの頂点を足します。片方は必ずゼロなので、有効になるのはどちらかひとつです。
#シェーダ
そんなわけでこうなりました。
Shader "Custom/debris" {
SubShader {
Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
ZWrite Off
Blend SrcAlpha OneMinusSrcAlpha // alpha blending
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 3.0
#include "UnityCG.cginc"
struct appdata_custom {
float4 vertex : POSITION;
fixed4 color : COLOR;
float4 texcoord : TEXCOORD0;
};
struct v2f
{
float4 pos:SV_POSITION;
fixed4 color:COLOR;
};
float4x4 _PrevInvMatrix;
float3 _TargetPosition;
float _Range;
float _RangeR;
float _MoveTotal;
float _Move;
v2f vert(appdata_custom v)
{
v.vertex.y += _MoveTotal;
float3 target = _TargetPosition;
float3 trip;
trip = floor( ((target - v.vertex.xyz)*_RangeR + 1) * 0.5 );
trip *= (_Range * 2);
v.vertex.xyz += trip;
float4 tv0 = v.vertex * v.texcoord.x;
tv0 = mul (UNITY_MATRIX_MVP, tv0);
v.vertex.y -= _Move;
float4 tv1 = v.vertex * v.texcoord.y;
tv1 = mul (UNITY_MATRIX_MV, tv1);
tv1 = mul (_PrevInvMatrix, tv1);
tv1 = mul (UNITY_MATRIX_P, tv1);
v2f o;
o.pos = tv0 + tv1;
float depth = o.pos.z * 0.02;
float normalized_depth = (1 - depth);
o.color = v.color;
o.color.a *= (normalized_depth);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
return i.color;
}
ENDCG
}
}
}
#動画
こうなりました
https://vimeo.com/139103011
動画のフレームレートが高くないんでアレですけど、実際に実行するとけっこう綺麗です。
#プロジェクトはこちら
https://github.com/dsedb/raindrop
#問題点
いろいろあります。
まず、前のフレームってのがどうか。可変フレームレートだと具合悪そう。なんらかの正規化が必要と思われます。ポーズしたときも、このままだと具合悪そう。
あと、カメラが高速で動くと単純に線が伸びるので、点を打つ場所が増えてしまって、光量が増してしまいますね。本来なら伸びた線の長さでアルファ値を正規化しないといかんと思います。
などなどあるんですけど、些細といえば些細。滑らかさの印象がぐっとよくなるので、
#積極的に使っていいと思います。
雨じゃなくても、数を減らして薄くして、空気中のゴミみたいなのの表現にしてもよいし。