知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】雪を降らせる【2】#39

前回の成果

雪を降らせるソースコードのStartまで理解できた。

soramamenatan.hatenablog.com


今回やること

前回の続きからやっていきます。

参考サイト様

qiita.com


ソースコード

void LateUpdate() {
    var targetPosition = Camera.main.transform.TransformPoint(Vector3.forward * _range);
    var renderer = GetComponent<Renderer>();
    renderer.material.SetFloat("_Range", _range);
    renderer.material.SetFloat("_RangeR", _rangeR);
    renderer.material.SetFloat("_Size", 0.1f);
    renderer.material.SetVector("_MoveTotal", _move);
    renderer.material.SetVector("_CamUp", Camera.main.transform.up);
    renderer.material.SetVector("_TargetPosition", targetPosition);
    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 * 2.0f);
    _move.y = Mathf.Repeat(_move.y, _range * 2.0f);
    _move.z = Mathf.Repeat(_move.z, _range * 2.0f);
}

Startは前回で理解できたので、LateUpdate関数の中身について学んでいきます。


LateUpdate()

これはUnityで定義している関数で、Update()の後に呼ばれるメソッドとなっています。
よく使用する例としては、Updateでキャラクターの移動を行い、LateUpdateでカメラの移動を行うなどです。


TransformPoint

ローカル座標からワールド座標へと変換する関数になります。


Mathf.PerlinNoise

2Dのパーリンノイズを生成する関数です。

// x : x座標
// y : y座標
PerlinNoise (float x, float y);

パーリンノイズとは

パーリンノイズとはノイズの一種です。

パーリンノイズ(英: Perlin noise)とは、コンピュータグラフィックスのリアリティを増すために使われるテクスチャ作成技法。擬似乱数的な見た目であるが、同時に細部のスケール感が一定である。このため制御が容易であり、各種スケールのパーリンノイズを数式に入力することで多彩なテクスチャを表現できる。

パーリンノイズとは - Weblio辞書:より引用


普通の乱数を使用してTextureに描画

f:id:soramamenatan:20200207161911j:plain

パーリンノイズ(PerlinNoise)とは【Unity】【パーリンノイズ】 - (:3[kanのメモ帳]:より引用

パーリンノイズを使用してTextureに描画

f:id:soramamenatan:20200207161914p:plain

Mathf-PerlinNoise - Unity スクリプトリファレンス:より引用

パーリンノイズを使用すると、滑らかになるのがわかります。
今回はこれを使用することにより、風を表現しています。


Mathf.Repeat

0から特定の値をループする関数になります。

// t : 移動させる値
// length : 指定の値
Repeat(float t, float length);

これを使用することにより_moveの値が無限に増えることを防いでいます。


これでソースコード側の解説が終わったので、次にShader側の解説を行います。
ソースコードの全容は前回の記事にあるので、今回は解説する部分だけ乗せます。

soramamenatan.hatenablog.com


ソースコード

v2f vert(appdata_custom v) {
    float3 target = _TargetPosition;
    float3 trip;
    float3 mv = v.vertex.xyz;
    mv += _MoveTotal;
    trip = floor(((target - mv) * _RangeR + 1) * 0.5f);
    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 = UnityObjectToClipPos(finalPosition);
    o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
    return o;
}

結構複雑な処理をしているように見えます。


頂点をリピートさせる

頂点を足し続けていると、カメラの描画範囲から外れてしまうので、カメラの範囲内に収まるようにリピートさせます。
これで何回リピートさせればいいかを求めます。

リピートさせる処理

下記のソースコードで行っています。
この部分が、このShaderでのキモと言えます。

float3 target = _TargetPosition;
float3 trip;
float3 mv = v.vertex.xyz;
mv += _MoveTotal;
trip = floor(((target - mv) * _RangeR + 1) * 0.5f);
trip *= (_Range * 2);
mv += trip;

下記でリピートの計算をしています。

trip = floor(((target - mv) * _RangeR + 1) * 0.5f);

図解をしていくので、図で使用している値を説明します。

意味 ソースでの変数名
0 原点 特になし
v ランダムな点 mv
v' ランダムな点のリピート結果 特になし
n リピート回数 trip
R カメラの範囲 _range
2R カメラの範囲の2倍 特になし
p カメラの位置 targetPosition


xyz軸で独立しているので、一次元の数直線で考えます。
まずは、

  • ランダムな点
  • 原点
  • カメラの範囲の2倍
  • カメラの位置

を図に落とし込みます。 カメラの範囲を2倍している理由は、カメラの範囲を

Random.Range(-_range, _range)

で定義しているからです。

図に落とし込む

f:id:soramamenatan:20200211111516p:plain

[Unity]雨粒を描画する - Qiita:より引用


次にランダムな点を2Rでリピートします。
これをpの付近になるまで行います。

リピート

f:id:soramamenatan:20200211111521p:plain

[Unity]雨粒を描画する - Qiita:より引用


これにより、pの付近に2つのv'を出せました。
pから±Rの範囲に入っているものが求める値となります。

p±Rの範囲

f:id:soramamenatan:20200211111525p:plain

[Unity]雨粒を描画する - Qiita:より引用


図から数式にする

これで何回リピートさせるかが図によって理解できました。
ソースにするために数式にします。

求めるべきv'は2Rをn回リピートさせたものなので、
v' = v + 2nR
となります。

そして、p+Rはv'より大きいので、
v' \lt p + R

上の二つの数式より、
v + 2nR \lt p + R
となります。
今回求めたいものは、nなので、
v + 2nR \lt p + R
= 2nR \lt p + R - v
= \displaystyle n \lt \frac{p + R - v}{2R}
= \displaystyle n \lt \frac{1}{2}((\frac{p - v}{R}) + 1)

となり、これを満たす最大の整数が求めるべきnとなります。
これをソースコードで表すと、

trip = floor(((target - mv) * _RangeR + 1) * 0.5f);

となり、nを求めることができます。
求めるものは整数なのでfloorを、除算は重いので乗算で計算しています。


雪をビルボードさせる

ビルボードとは、2DのTextureが3D空間で常にカメラの方向を向くようにすることです。
これをすることにより、カメラの座標がどこでも雪のTextureを正しく描画することができます。


ビルボードの処理

以下がビルボードの処理を行っているソースコードです。

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;


ObjSpaceViewDir

指定されたローカル座標の頂点から、カメラへの正規化されていないベクトルを返す関数です。

以下定義となります。

inline float3 ObjSpaceViewDir( in float4 v )
{
    float3 objSpaceCameraPos = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos.xyz, 1)).xyz;
    return objSpaceCameraPos - v.xyz;
}

これを使用することによって、視線ベクトルを取得することができます。


雪のTextureの四隅を判断する

カメラのx軸とy軸を取得

ビルボードをするために、カメラのx軸とy軸を取得する必要があります。

float3 diff = _CamUp * _Size;

これによりdiffにはカメラのy軸のベクトルの値が入っています。
ですので、カメラのx軸のベクトルを取得します。

eyeVectorには、オブジェクトからカメラに向かう視線ベクトルが入っています。
ですので、diffとの外積を取ることによりカメラのx軸のベクトルを取得することができます。

各ベクトルのイメージ

f:id:soramamenatan:20200211134651p:plain


雪のTexture座標の計算

カメラの軸を利用して四隅の頂点の位置を計算します。

下記のソースコードで座標の計算を行っています。

tv0 += (v.texcoord.x - 0.5f) * sideVector * _Size;
tv0 += (v.texcoord.y - 0.5f) * diff;

Script側でUV値を以下のソースコードで指定していたので、

_uvs[i * 4 + 0] = new Vector2(0.0f, 0.0f);
_uvs[i * 4 + 1] = new Vector2(1.0f, 0.0f);
_uvs[i * 4 + 2] = new Vector2(0.0f, 1.0f);
_uvs[i * 4 + 3] = new Vector2(1.0f, 1.0f);

-0.5をすることにより、texcoordで四隅を判定することができます。


fragment shaderに伝える

これが最後の処理になります。

v2f o;
o.pos = UnityObjectToClipPos(finalPosition);
o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
return o;

上記で計算してきたものをfragment shaderに渡してあげます。

MultiplyUV, UNITY_MATRIX_TEXTURE0

どちらも明確なものがなかったので、調べて雰囲気で理解したことを記載します。

MultiplyUV

定義は以下となります。

float2 MultiplyUV (float4x4 mat, float2 inUV) {
    float4 temp = float4 (inUV.x, inUV.y, 0, 0);
    temp = mul (mat, temp);
    return temp.xy;
}

以下のようにすることにより、高速化するらしい?

o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);

ちなみに表示結果は以下でも変わらなかったです。

o.uv = v.texcoord;

answers.unity.com


UNITY_MATRIX_TEXTURE0

テクスチャの転置行列のことです。


結果

雪が降れば完成です。

f:id:soramamenatan:20200211142044g:plain


ソースコードにコメントを付与

Shader "Unlit/fallSnow" {
    Properties {
        _MainTex("MainTex", 2D) = "white" {}
    }
    SubShader {
        Tags {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
        }
        ZWrite Off
        Cull Off
        Blend SrcAlpha OneMinusSrcAlpha

        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;

            v2f vert(appdata_custom v) {
                float3 target = _TargetPosition;
                float3 trip;
                float3 mv = v.vertex.xyz;
                mv += _MoveTotal;
                // リピート数を計算する
                // mv + 2 * _Range * n回 < _TargetPosition + _Rangeより
                trip = floor(((target - mv) * _RangeR + 1) * 0.5f);
                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));
                // カメラのx軸ベクトル
                float3 sideVector = normalize(cross(eyeVector, diff));
                // 雪のx軸の計算
                tv0 += (v.texcoord.x - 0.5f) * sideVector * _Size;
                // 雪のy軸の計算
                tv0 += (v.texcoord.y - 0.5f) * diff;
                finalPosition = tv0;

                v2f o;
                o.pos = UnityObjectToClipPos(finalPosition);
                // 高速化
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
                return o;
            }

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

今回はこれで終了です。
ここまでご視聴ありがとうございました。