🌈

そろそろShaderをやるパート73 蝶々がはばたく表現

2022/05/01に公開

そろそろShaderをやります

そろそろShaderをやります。そろそろShaderをやりたいからです。
パート100までダラダラ頑張ります。10年かかってもいいのでやります。
100記事分くらい学べば私レベルの初心者でもまあまあ理解できるかなと思っています。

という感じでやってます。

※初心者がメモレベルで記録するので
 技術記事としてはお力になれないかもしれません。

下準備

下記参考
そろそろShaderをやるパート1 Unite 2017の動画を見る(基礎知識~フラグメントシェーダーで色を変える)

デモ

蝶々がはばたくデモです。
蝶々の動きはShader、移動や大量に生成される部分についてはParticleで行っています。

Shaderサンプル

Shader "Custom/Butterfly"
{
    Properties
    {
        [NoScaleOffset]_MainTex ("Texture", 2D) = "white" {}
        [HDR]_MainColor("MainColor",Color) = (1,1,1,1)
        _FlapSpeed ("Flap Speed", Range(0,20)) = 10
        _FlapIntensity ("Flap Intensity", Range(0,2)) = 1
        _MoveSpeed ("Move Speed", Range(0,5)) = 1
        _MoveIntensity ("Move Intensity", Range(0,1)) = 0.2
        _RandomFlap ("Random Flap", Range(1,2)) = 1
    }
    SubShader
    {
        Tags
        {
            "RenderType"="Tansparent"
        }

        Pass
        {
            Cull off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag


            #include "UnityCG.cginc"

            struct appdata
            {
                float2 uv : TEXCOORD0;
                //中心座標を受け取る変数
                float3 center : TEXCOORD1;
                //ランダムな値を受け取る変数
                float random : TEXCOORD2;
                //速度を受け取る変数
                float3 velocity : TEXCOORD3;
                float4 color : COLOR;
                float4 vertex : POSITION;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                float4 color : COLOR;
            };

            sampler2D _MainTex;
            float4 _MainColor;
            float _FlapSpeed;
            float _FlapIntensity;
            float _MoveIntensity;
            float _MoveSpeed;
            float _RandomFlap;

            //ランダムな値を返す
            float rand(float2 co) //引数はシード値と呼ばれる 同じ値を渡せば同じものを返す
            {
                return frac(sin(dot(co.xy, float2(12.9898, 78.233))) * 43758.5453);
            }

            //非線形ブラウン運動を計算する
            float fbm(float x, float t)
            {
                return sin(x + t) + 0.5 * sin(2.0 * x + t) + 0.25 * sin(4.0 * x + t);
            }

            v2f vert(appdata v)
            {
                v2f o;

                //ローカル座標
                //Particle SystemのGameObjectが存在するところが原点となり、vertexにはこの原点から見た座標が入ってくる
                //そのため、パーティクルの中心座標を引いて計算を行い、もとに戻すという工程を踏む
                float3 local = v.vertex - v.center;

                //ランダムな値を計算
                float randomFlap = lerp(_FlapSpeed / _RandomFlap, _FlapSpeed, rand(v.random));
                float flap = (sin(_Time.w * randomFlap) + 0.5) * 0.5 * _FlapIntensity;
                //Sign(x)はxが0より大きい場合は1、小さい場合は-1を返す
                //これにより、x=0となる箇所から線対称に回転を計算できる
                half c = cos(flap * sign(local.x));
                half s = sin(flap * sign(local.x));
                /*       |cosΘ -sinΘ|
                  R(Θ) = |sinΘ  cosΘ|  2次元回転行列の公式*/
                half2x2 rotateMatrix = half2x2(c, -s, s, c);

                //羽の回転を反映
                local.xy = mul(rotateMatrix, local.xy);

                //進行方向を向かせるための回転行列を作成
                //正面は進行方向、すなわちParticleから取得したvelocity
                float3 forward = normalize(v.velocity);
                float3 up = float3(0, 1, 0);
                float3 right = normalize(cross(forward, up));

                //行列を作成
                //どうやら変数に詰めるときだけ行オーダーになっている?っぽい
                //なのでtransposeで転置を行う
                //すなわち以下でも可
                //float3x3 mat = float3x3(right.x,up.x,forward.x,
                //                        right.y,up.y,forward.y,
                //                        right.z,up.z,forward.z);
                float3x3 mat = transpose(float3x3(right, up, forward));

                //Velocity(正面方向)に応じた回転を反映
                v.vertex.xyz = mul(mat, local);

                //原点をもとの座標に戻す
                v.vertex.xyz += v.center;
                o.vertex = UnityObjectToClipPos(v.vertex);
                //上下の移動量を求めて反映 ワールド座標系で上下移動させる
                float move = fbm(87034 * v.random, _Time.w * _MoveSpeed) * _MoveIntensity;
                o.vertex.y += move;
                o.uv = v.uv;
                //頂点カラー
                o.color = v.color;
                return o;
            }

            float4 frag(v2f i) : SV_Target
            {
                //PlaneのZ軸が正の方向になるようにテクスチャーをサンプリング
                //テクスチャーをRepeatにしておく必要あり
                float4 col = tex2D(_MainTex, -i.uv);
                col.rgb *= _MainColor.rgb;
                //頂点カラーを適用 これでParticleの色を拾うようになる
                col *= i.color;
                //重なったところが透明に切り抜かれてしまうので透過領域をClipしておく
                clip(col.a - 0.01);
                return col;
            }
            ENDCG
        }
    }
}

Custom Vertex Streamsで利用する値

前提として、このShaderはParticle SystemのCustom Vertex Streamsを利用します。
【参考リンク】:そろそろShaderをやるパート46 ParticleからShaderへ値を渡す

そのため、通常のPlaneに適用しても以下GIFのような挙動になります。

この前提を踏まえたうえでShaderを見ていくと、
3つのパラメーターをParticle Systemから受け取っていることがわかります。

struct appdata
{
    float2 uv : TEXCOORD0;
    //中心座標を受け取る変数
    float3 center : TEXCOORD1;
    //ランダムな値を受け取る変数
    float random : TEXCOORD2;
    //速度を受け取る変数
    float3 velocity : TEXCOORD3;
    float4 color : COLOR;
    float4 vertex : POSITION;
};

受け取っているのは具体的には以下です。
・個々のパーティクルの中心座標
・個々のパーティクルが持つランダムな値
・個々のパーティクルの速度を表すベクトル

パーティクル側の設定は以下です。
ほしい値がTEXCOORDに入ってくるように不要な値を入れて順番を調整しています。
これはコードを読む際にわかりやすくするためなので、
必要な値だけCustom Vertex Streamsに設定するという使い方でも問題ないです。

パーティクルの中心について

各パーティクルの中心座標を用いて計算するために以下のような処理を行っています。

float3 local = v.vertex - v.center;
~~
v.vertex.xyz += v.center;

この理由としてはParticle SystemのGameObjectが存在するところが原点となり、
vertexにはこの原点から見た座標が入ってくるからです。

そのため、一度各パーティクルの頂点座標を
パーティクルの中心から見た座標に計算し直してから、
元の頂点の位置に戻す、、、という工程を踏んでいます。

これにより、後述の各パーティクルにおける線対象な羽の動き
各パーティクルの正面方向に対する回転が可能となります。

羽の動き

羽の動きは頂点シェーダーが担います。
Planeを線対象に上下に回転させれば羽っぽくなります。

図解すると以下です。

図の中でX軸における座標が0以上か0以下かを基準に線対象にわけていますが、
これはsignという関数で実現できます。
sign(x)はxが0より大きい場合は1、小さい場合は-1を返します。

float3 local = v.vertex - v.center;

~~

//Sign(x)はxが0より大きい場合は1、小さい場合は-1を返す
//これにより、x=0となる箇所から線対称に回転を計算できる
half c = cos(flap * sign(local.x));
half s = sin(flap * sign(local.x));
/*       |cosΘ -sinΘ|
  R(Θ) = |sinΘ  cosΘ|  2次元回転行列の公式*/
half2x2 rotateMatrix = half2x2(c, -s, s, c);

//羽の回転を反映
local.xy = mul(rotateMatrix, local.xy);

回転について

個々のパーティクル、すなわち蝶々に回転を与えたときには破綻のない動きが必要です。
これを実現するために速度(velocity)のベクトルを正面と定義し、
その正面に対して蝶々が頭及び体を回転するように計算を行います。

そのために必要なのが回転行列です。
回転行列は以下のように3つの直交する単位ベクトルで表すことができます。

【引用元】:バイオメカニクスにおけるモーションセンサの利用

私はこの蝶々を作るにあたって先行資料に目を通すまで、
以下のような行列が回転行列であるという固定概念がありました。
しかし、これらもよく見ると3つの直交する単位ベクトルが含まれています。
"X軸を中心に"という部分が1列目の(1,0,0)という単位ベクトルで表現されています。

  • X軸を中心にθ度回転させる場合
\begin{pmatrix} x' \\ y' \\ z' \\ 1\\ \end{pmatrix} = \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & cosθ & -sinθ & 0 \\ 0 & sinθ & cosθ & 0 \\ 0 & 0 & 0 & 1\\ \end{pmatrix} \begin{pmatrix} x \\ y \\ z \\ 1 \\ \end{pmatrix}

話を戻すと、"蝶々を正面に対して頭及び体を回転するように計算を行う"と前述しました。
これを"回転行列は3つの直交する単位ベクトルで表すことができる"
という性質を利用して解決します。

すなわち、回転後の3つの直交する単位ベクトルがわかればよいということです。

今回、速度(velocity)のベクトルを正面と定義するので、
1つ目の単位ベクトルはいきなり解決します。

次に回転後の空間における上方向のベクトルを(0,1,0)として定めます。
これで2つの単位ベクトルが求まりました。

この2つの単位ベクトルは直交します。
よって、もう1つのベクトルは外積によって求めることができます。

ここまでの流れが以下の箇所です。

//進行方向を向かせるための回転行列を作成
//正面は進行方向、すなわちParticleから取得したvelocity
float3 forward = normalize(v.velocity);
float3 up = float3(0, 1, 0);
float3 right = normalize(cross(forward, up));

//行列を作成
//どうやら変数に詰めるときだけ行オーダーになっている?っぽい
//なのでtransposeで転置を行う
//すなわち以下でも可
//float3x3 mat = float3x3(right.x,up.x,forward.x,
//                        right.y,up.y,forward.y,
//                        right.z,up.z,forward.z);
float3x3 mat = transpose(float3x3(right, up, forward));

//Velocity(正面方向)に応じた回転を反映
v.vertex.xyz = mul(mat, local);

transposeというは行列の転置を行ってくれる関数です。
基本的にShaderは計算時に列オーダーで計算を行ってくれる認識でしたが、
変数に詰めるときだけ行オーダーになっており、transposeを利用しました。

わざわざ使う必要はありませんが、
ちょっとややこしいのでまた詰まった時のためにメモとして残しておきます。

非線形ブラウン運動

蝶々のフラフラした動きを再現するのに複雑な計算式を用いています。

float fbm(float x, float t)
{
    return sin(x + t) + 0.5 * sin(2.0 * x + t) + 0.25 * sin(4.0 * x + t);
}

これにより、Sin波が不規則にノイズ交じりの計算結果を返します。

Velocityの設定

蝶々の進行方向についてはParticle SystemのVelocity over Lifetimeを使用します。

以下のようにRandom Betweenを適用して許容できる範囲でランダム性を持たせると
いい感じになります。OrbitalでY軸に対して回転させる処理も加えてみました。

参考リンク

【Unity】Particle Systemでvertexとtexcoordで回転アニメーション
フラクタルブラウン運動とドメインワープ
シェーダーを活用した3Dライブ演出のアップデート ~『ラブライブ!スクールアイドルフェスティバル ALL STARS』(スクスタ)の開発事例~​
[Unity][URP] Y軸ビルボードシェーダー
空間とプラットフォームの狭間で – Unityの座標変換にまつわるお話 –

Discussion