なおしのこれまで、これから

学んだこと・感じたこと・やりたいこと

Unityのシェーダーでマトリックスの流れる暗号を書く

f:id:vxd-naoshi-19961205-maro:20211007011045g:plain

始めに

前回マトリックスのパーティクルを作成しました。

shitakami.hatenablog.com


このままノリで映画の画面などで表示される模様をシェーダーで書いてみようと思います。

また、今回の内容はこちらのリポジトリに入っております。

github.com



やったこと

ここでは簡単に作成の流れを書いていきます。


1. 文字のテクスチャを用意

前回のマトリックスパーティクルで作った文字テクスチャを使いまわしました。

Google Slideで文字を打ち込んでスクショしたものです。

f:id:vxd-naoshi-19961205-maro:20211006230003p:plain



2. Quadに貼り付け

まずはUnlitShaderの新規作成、マテリアルの作成をします。

あとはマテリアルをQuadにつけて、先ほどの文字のテクスチャを設定します。

f:id:vxd-naoshi-19961205-maro:20211006230333p:plain



3. 1文字だけを表示させる

2.の状態では文字すべて表示されていたので、Tilingの計算を用いて一文字だけの表示を行います。

計算は float4(x, y) に文字の大きさ、 (z, w) に初期位置を設定します。

後は文字テクスチャのIndexを渡して計算を行っています。

float letterSizeX = _TilingAndOffset.x;
float letterSizeY = _TilingAndOffset.y;

i.uv.x *= _TilingAndOffset.x;
i.uv.y *= _TilingAndOffset.y;
i.uv.x += _TilingAndOffset.z + letterSizeX * indexX;
i.uv.y += _TilingAndOffset.w - letterSizeY * indexY;


f:id:vxd-naoshi-19961205-maro:20211006231908p:plain



4. 文字をマス目上に表示する

文字が1文字しか表示されていないので、これをマス目上に表示します。

与えられた値の小数部分を取得できる frac() で簡単にマス目を作ることができます。

i.uv = frac(i.uv * _RowCount);

f:id:vxd-naoshi-19961205-maro:20211006233236p:plain



5. マス目毎に文字を変える

全部同じ文字だけですと味気ないので、マス目毎に文字を変えます。

与えられた値の整数部分を取得できる floor() を使い、マスのインデックスを計算して文字のインデックスを求めます。

uint maxIndex = _MaxIndexX * _MaxIndexY;

float r = floor(i.uv.x * _RowCount) + _Index;
float c = floor(i.uv.y * _RowCount) - _Index;
                
uint index = (_Index + r + c * _MaxIndexX) % maxIndex;
uint indexX = index % _MaxIndexX;
uint indexY = index / _MaxIndexX;

f:id:vxd-naoshi-19961205-maro:20211006234726p:plain



6. 上から1→0 の値を流す

次にアニメーションに必要な値を流します。

マトリックスの流れる暗号では上から下に文字の色が変化していきます。

この値を uv.yfrac() を使って計算します。

また、値の流れる間隔や速さを変えられるようにします。

fixed value = frac(i.uv.y * _Period + _Time.y * _TimeSpeed);


f:id:vxd-naoshi-19961205-maro:20211007001609g:plain



7. 値に応じて色を変える

先ほどの値をもとに色を変えます。

値が大きいときは白色を強くし、値が小さいときはアルファ値を下げるよう計算します。

雑に計算しているのでもしかするともっと良い計算法があるかもしれません。

fixed alphaRate = saturate(1 - frac(i.uv.y * _Period + _Time.y * _TimeSpeed) * _EraseSpeed);
fixed rate = saturate((alphaRate - _WhiteColorThreshold)/(1 - _WhiteColorThreshold));
col = lerp(fixed4(_BaseLetterColor.xyz, alphaRate), float4(1, 1, 1, alphaRate), rate);


f:id:vxd-naoshi-19961205-maro:20211007002555g:plain



8. 列ごとに値の流れをずらす

均一に色が変わっているので、列ごとに流れをずらします。

列のインデックスを求めて乱数でずらす値を求めます。

half column = floor(i.uv.x * _RowCount);
half periodOffset = GetRandomNumber(float2(column + _PeriodSeed, -column), _PeriodSeed) * 100;


f:id:vxd-naoshi-19961205-maro:20211007003417g:plain



9. 細かい修正

あとは細かい修正をします。

今は文字が固定なので、ランダムで変わるようにしたり、合わせて回転や反転をするようにします。

最後にパラメータ調整を行います。



完成品

ざっくりと解説しましたが、最終的なシェーダーはこちらになります。

MatrixTexture.shader

Shader "Unlit/MatrixTexture"
{
    Properties
    {
        [NoScaleOffset]_MainTex ("Texture", 2D) = "white" {}
        _TilingAndOffset("Tiling And Offset", Vector) = (1, 1, 0, 0)
        _RowCount ("Row Count", int) = 1

        _MaxIndexX ("Max Index X", int) = 0
        _MaxIndexY ("Max Index Y", int) = 0
        _Index ("Index", int) = 0

        _TimeSpeed ("Time Speed", float) = 1
        _Period ("Period", float) = 1
        _PeriodSeed ("Period Seed", float) = 0
        _EraseSpeed ("Erase Speed", float) = 1

        _BaseLetterColor ("Base Letter Color", Color) = (1, 1, 1, 1)
        _WhiteColorThreshold ("White Color Threshold", float) = 1
        _DiscardThreshold ("Discard Threashold", Range(0, 1)) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue" = "Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha 
        LOD 100


        Pass
        {

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"
            #define PI 3.141592

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _TilingAndOffset;
            uniform uint _RowCount;
            uniform uint _MaxIndexX;
            uniform uint _MaxIndexY;
            uniform uint _Index;
            uniform fixed _DiscardThreshold;
            uniform half _TimeSpeed;
            uniform half _Period;
            uniform half _PeriodSeed;
            uniform half _EraseSpeed;
            uniform fixed4 _BaseLetterColor;
            uniform fixed _WhiteColorThreshold;

            float GetRandomNumber(float2 texCoord, int Seed)
            {
                return frac(sin(dot(texCoord.xy, float2(12.9898, 78.233)) + Seed) * 43758.5453);
            }

            float2 RotateUV(float2 uv, float theta, uint xReverseFlag, uint yReverseFlag)
            {
                 half2x2 mat = half2x2(cos(theta), -sin(theta), sin(theta), cos(theta));

                uv = uv - 0.5;
                uv = mul(uv, mat) + 0.5;
                
                uv.x = uv.x * (1 - xReverseFlag) + (1 - uv.x) * xReverseFlag;
                uv.y = uv.y * (1 - yReverseFlag) + (1 - uv.y) * yReverseFlag;

                return uv;
            }

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv;

                uint maxIndex = _MaxIndexX * _MaxIndexY;

                float r = floor(i.uv.x * _RowCount) + _Index;
                float c = floor(i.uv.y * _RowCount) - _Index;
                float rnd = GetRandomNumber(float2(r, c), 0);
                float timeOffset = GetRandomNumber(float2(r, -c), 0);
                uint index = rnd * 10000 + _Time.w + timeOffset;
                index = (index + _Index) % maxIndex;
                
                uint indexX = index % _MaxIndexX;
                uint indexY = index / _MaxIndexX;

                float2 gridUV = frac(i.uv * _RowCount);

                float letterSizeX = _TilingAndOffset.x;
                float letterSizeY = _TilingAndOffset.y;

                gridUV.x *= _TilingAndOffset.x;
                gridUV.y *= _TilingAndOffset.y;
                gridUV.x += _TilingAndOffset.z + letterSizeX * indexX;
                gridUV.y += _TilingAndOffset.w - letterSizeY * indexY;

                half theta = -PI + step(index, 30) * PI + step(index, 60) * PI + step(index, 90) * PI;

                fixed xReverseFlag = step(0.5, rnd);
                fixed yReverseFlag = step(index, 60);
                gridUV = RotateUV(gridUV, theta, xReverseFlag, yReverseFlag);

                fixed4 col = tex2D(_MainTex, gridUV);
                if(col.b > _DiscardThreshold)
                    discard;

                half column = floor(i.uv.x * _RowCount);
                half periodOffset = GetRandomNumber(float2(column + _PeriodSeed, -column), _PeriodSeed) * 100;
                fixed alphaRate = saturate(1 - frac(uv.y * _Period + _Time.y * _TimeSpeed + periodOffset) * _EraseSpeed);
                fixed rate = saturate((alphaRate - _WhiteColorThreshold)/(1 - _WhiteColorThreshold));
                col = lerp(fixed4(_BaseLetterColor.xyz, alphaRate), float4(1, 1, 1, alphaRate), rate);
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}


こちらのgifには +α でPost ProcessingのBloomをつけています。

f:id:vxd-naoshi-19961205-maro:20211007011045g:plain



最後に

マトリックスパーティクルを作ったときもそうですが、今回も完成品が出来たときはやっぱり感動しました。

久々に作ってみたいものを作ると童心が戻った気がします。

また時間があるときに追加で遊んだものも記事にまとめようと思います。


追記

続きです。

shitakami.hatenablog.com



参考

今回使った計算法などはこちらのスライドがもとになっています。

docs.google.com