したかみ ぶろぐ

Unity成分多め

Unityで映画Matrixになれるシェーダを作る

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

始めに

前回、前々回の記事です。

shitakami.hatenablog.com

shitakami.hatenablog.com


今回の内容はこの2つを組み合わせたものです。特に目新しいことはないかもしれません。

また、今回の内容もリポジトリに追加しました。

github.com



シェーダの作成

やったことは主に次のようになります。

  1. Triplanarシェーダの作成
  2. テクスチャマッピング部分をマトリックスを書く処理に置き換える
  3. 微調整 (描画順の調整)


1. Triplanarシェーダの作成

前回のシェーダをそのまま使います。

下の画像は文字のテクスチャをTriplanarで描画したものになります。

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



2. テクスチャマッピング部分をマトリックスを書く処理に置き換える

Triplanarにマトリックステクスチャを組み合わせます。

そのまま組み合わせるとおかしくなる部分があったので、そこを軽く修正しました。

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



3. 微調整 (描画順の調整)

今のままではオブジェクトが透過されているので、オブジェクトとその後ろにあるオブジェクトとの文字が重なってオブジェクト毎の輪郭が分かりにくくなってました。

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



なので、裏側の文字が描画されないよう調整しました。

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



実装

シェーダのコードはこのようになりました。

Matrix.shader

Shader "Unlit/Matrix"
{
    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
        _Sharpness ("Sharpness", float) = 1
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        Blend SrcAlpha OneMinusSrcAlpha 
        LOD 100
        
        Pass
        {
            ZWrite ON
            ColorMask 0
        }
        Pass
        {
            ZWrite OFF
            Ztest LEqual
            
            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;
                float3 normal : NORMAL;
            };

            struct v2f
            {
                float4 vertex : SV_POSITION;
                float3 worldPos : TEXCOORD0;
                float3 normal : TEXCOORD1;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float4 _TilingAndOffset;
            uniform float _RowCount;
            uniform float _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;
            uniform float _Sharpness;
            
            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;
            }

            fixed4 MatrixTex(float2 uv)
            {
                float2 gridUV = frac(uv * _RowCount);
                uint maxIndex = _MaxIndexX * _MaxIndexY;
                
                float r = floor(uv.x * _RowCount) + _Index;
                float c = floor(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;
                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);
                 
                half column = floor(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));
                if(col.b > _DiscardThreshold)
                    alphaRate = 0;
                
                return lerp(fixed4(_BaseLetterColor.xyz, alphaRate), float4(1, 1, 1, alphaRate), rate);
            }
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
                o.worldPos = worldPos.xyz;
                o.normal = abs(UnityObjectToWorldNormal(v.normal));
                o.normal = pow(o.normal, _Sharpness);
                o.normal = o.normal / (o.normal.x + o.normal.y + o.normal.z);
                
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float2 uv_front = i.worldPos.xy;
                float2 uv_side = i.worldPos.zy;
                float2 uv_top = i.worldPos.xz;
                fixed4 col_front = MatrixTex(uv_front) * i.normal.z;
                fixed4 col_side = MatrixTex(uv_side) * i.normal.x;
                fixed4 col_top = MatrixTex(uv_top) * i.normal.y;
                return col_front + col_side + col_top;
            }
            ENDCG
        }
    }
}



雑に遊ぶ

シェーダができたので、テキトーにオブジェクトに貼り付けて遊んでいきます。

まずはレイトレでよく見るあの小部屋を用意しました。

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


これに先ほどのシェーダを適用したマテリアルを用意します。また、壁用とオブジェクト用の2つ用意しました。

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



次にたまたま持っていたAssetにマテリアルをつけてみます。

assetstore.unity.com


結果はこのようになりました。個人的にマトリックス最新PVのワンシーンみたいになったと感じました。

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



最後に

いつかやりたいなーっと思っていたことを達成できました。

このシェーダで色々遊ぶことができそうですが、それはまたやりたいとなったら遊びます。

また、最後に最新のマトリックスが楽しみなので、PVを貼ります。


www.youtube.com



参考

元々はこのツイートを見ていつかやりたいなと思ってました。

また、こちらからTriplanarを使うやり方を真似させて頂きました。

Unityで平面マッピング・Triplanarをする

始めに

shitakami.hatenablog.com

前回作成したマトリックスの模様をどうにかオブジェクトに貼り付けたいなと思い、調べたところ見つかったのが平面マッピングとTriplanarでした。

今回はこちらについてまとめようと思います。



平面マッピング(Planar)

「平面マッピング」という単語を調べたところ、wikipediaではこのように記載されていました。

テクスチャを貼り付けた平面を物体が存在する座標系上に配置し、それを物体に投影する。

個人的にはこの文章を読んでもよくわからないかったのですが、私なりに説明しますとテクスチャをワールド座標系の一方向から投影するという認識です。(間違っているかもしれません)


実装

基本的にテクスチャはUV座標をもとに貼り付けられますが、平面マッピングではワールド座標系の頂点座標をもとにテクスチャを貼り付けます。

なので、やることはほとんど頂点シェーダで完結します。 以下のシェーダではXZ平面でマッピングを行います。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.uv = TRANSFORM_TEX(worldPos.xz, _MainTex);
    return o;
}
            
fixed4 frag (v2f i) : SV_Target
{
    // sample the texture
    fixed4 col = tex2D(_MainTex, i.uv);
    // apply fog
    UNITY_APPLY_FOG(i.fogCoord, col);
    return col;
}


このシェーダでマテリアル作成し、テキトーにオブジェクトに貼り付けたものが次のgifになります。

見て分かる通り、ワールド座標系に貼り付けているのでオブジェクトを移動するとオブジェクトの模様も動きます。

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



しかし、一方向からのマッピングなので、投影方向と垂直でない面はテクスチャが伸びてしまいます。

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

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



Triplanar

この単語の日本語訳が見つからなかったので、私なりの訳は「三方向からの平面マッピング」です。

先ほどの平面マッピングでは一方向からのみでしたが、Triplanarでは三方向からマッピングを行うため、テクスチャが伸びてしまう現象を抑えることができます。

まずは単純に三方向からマッピングをするシェーダになります。

v2f vert (appdata v)
{
    v2f o;
    o.vertex = UnityObjectToClipPos(v.vertex);
    float4 worldPos = mul(unity_ObjectToWorld, v.vertex);
    o.worldPos = worldPos.xyz;
    o.normal = abs(UnityObjectToWorldNormal(v.normal));
    o.normal = o.normal / (o.normal.x + o.normal.y + o.normal.z);
    
    return o;
}

fixed4 frag (v2f i) : SV_Target
{
    float2 uv_front = TRANSFORM_TEX(i.worldPos.xy, _MainTex);
    float2 uv_side = TRANSFORM_TEX(i.worldPos.zy, _MainTex);
    float2 uv_top = TRANSFORM_TEX(i.worldPos.xz, _MainTex);
    // apply fog
    fixed4 col_front = tex2D(_MainTex, uv_front) * i.normal.z;
    fixed4 col_side = tex2D(_MainTex, uv_side) * i.normal.x;
    fixed4 col_top = tex2D(_MainTex, uv_top) * i.normal.y;
    fixed4 col = col_front + col_side + col_top;
    return col;
}



このシェーダをアタッチしたものがこちらになります。

三方向からテクスチャがブレンドされていることがわかります。


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



このままですと、見栄えが良くないので法線ベクトルをもとにどの方向からのテクスチャを描画するか判別します。

また、_Sharpness 変数を用意して、pow(normals, _Sharpness) を計算することで小さい値はより小さくなり、一番面が向いている方向に対してのマッピングが行われるようになります。

o.normal = abs(UnityObjectToWorldNormal(v.normal));
o.normal = pow(o.normal, _Sharpness);
o.normal = o.normal / (o.normal.x + o.normal.y + o.normal.z);


この計算を追加したシェーダをアタッチしたものがこちらになります。

テクスチャが重なっている個所が少なくなり、またマッピングされる方向の境目がある程度目立たなくなります。

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



最後に

平面マッピング頂点座標でテクスチャを貼るだけとても簡単にできるとわかりました。

今回は平面マッピングのみをまとめましたが、これを使うことでテクスチャが伸びてしまう現象などの解決策になることがわかりました。


次回はマトリックスのテクスチャと組み合わせたものについてまとめようと思います。



参考

英語の記事になりますが、とても丁寧でわかりやすかったです。

www.ronja-tutorials.com

www.ronja-tutorials.com


こちらは日本語のTriplanarの記事になります。

qiita.com

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

UniRxのオペレータCombineLatestとWithLatestFrom

始めに

2つのストリームを合成する際に、CombineLatestWithLatestFromを混同していたので一度自分なりに調べてみようと思います。



CombineLatest

こちらのオペレータの特徴はこのようになっています。

  1. 複数のIObservableをまとめられる
  2. すべてのIObservableから1回以上、値が流れたら後続に流し始める
  3. 値を流し始めたあと、どれか1つのIObservableから値を受け取ったらそのIObservableからの値だけを更新して後続に流す。


1. 複数のIObservableをまとめられる

引数に複数のIObservableを渡せますし、IEnumerable<IObservable<T>>の後続に引数なしでCombineLatestをつけることですべてのIObservable<T>をまとめることができます。

また、7つまでの合成であれば合成するためのラムダ式を記述して無名関数として渡すことができます。

UniRx/Observable.Concatenate.cs at 284d5c50d3f1ddd9fa7df3d382ea904732a9c2ff · neuecc/UniRx · GitHub より引用

        public static IObservable<TResult> CombineLatest<TLeft, TRight, TResult>(this IObservable<TLeft> left, IObservable<TRight> right, Func<TLeft, TRight, TResult> selector)
        {
            return new CombineLatestObservable<TLeft, TRight, TResult>(left, right, selector);
        }


        public static IObservable<IList<T>> CombineLatest<T>(this IEnumerable<IObservable<T>> sources)
        {
            return CombineLatest(sources.ToArray());
        }


        public static IObservable<IList<TSource>> CombineLatest<TSource>(params IObservable<TSource>[] sources)
        {
            return new CombineLatestObservable<TSource>(sources);
        }


        public static IObservable<TR> CombineLatest<T1, T2, T3, TR>(this IObservable<T1> source1, IObservable<T2> source2, IObservable<T3> source3, CombineLatestFunc<T1, T2, T3, TR> resultSelector)
        {
            return new CombineLatestObservable<T1, T2, T3, TR>(source1, source2, source3, resultSelector);
        }

. . . . .
. . . . .



2. すべてのIObservableから1回以上、値が流れたら後続に流し始める

下の図のように合成しているIObservableのすべてから1回以上値が流れないとCombineLatestから値が流れ始めません。

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



3. 値を流し始めたあと、どれか1つのIObservableから値を受け取ったらそのIObservableからの値だけを更新して後続に流す。

値を流し始めたあとはどれか1つから値が流れたらその値だけを更新し、それ以外からの値は最新のものを使って後続に値を流します。

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

WithLatestFrom

こちらのオペレータの特徴は次のようになっています。

  1. 2つのIObservable(メイン、サブ)をまとめられる
  2. サブのIObservableから1回以上値が流されている状態でメインから流れた時に値を流し始める
  3. 値を流し始めたあと、メインのIObservableから値が渡されたときのみ後続に値を流す。



1. 2つのObservable(メイン、サブ)をまとめられる

WithLatestFromではメインとなるIObservableとサブとなるIObservableの2つをまとめられます。 それ以上のObservableをまとめることはできません。

UniRx/Observable.Concatenate.cs at 284d5c50d3f1ddd9fa7df3d382ea904732a9c2ff · neuecc/UniRx · GitHub より引用

        public static IObservable<TResult> WithLatestFrom<TLeft, TRight, TResult>(this IObservable<TLeft> left, IObservable<TRight> right, Func<TLeft, TRight, TResult> selector)
        {
            return new WithLatestFromObservable<TLeft, TRight, TResult>(left, right, selector);
        }



2. サブのObservableから1つ以上値が流されている状態でメインから流れた時に値を流し始める

サブから1つ以上の値が流れた状態でないと、メインからいくら値が流されても値が流れません。

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



3. 値を流し始めたあと、メインのObservableから値が渡されたときのみ後続に値を流す。

値が流れている状態ではメインから値が流れたときのみ、後続に値を流します。 値を流すときは最新のサブからの値とメインからの値を合成します。

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



2つの比較

CombineLatest WithLatstFrom
合成できる個数 2つ以上 2つ(メインとサブのIObservable)のみ
値を流し始める基準 すべてのIObservableから1回以上値が流れる サブから1回以上値が流れている状態でメインから値が流れる
値を流す基準 どれか一つのIObservableから値が流れる メインから値が流れる


使い分け

言わずもがな2つより多いIObservableを合成する場合はCombineLatestを使うことになるでしょう。

2つのObservableを合成するときでも、両方のIObservableで発火したい場合はCombineLatest、1つのIObservableからのみ発火したい場合はWithLatestFromになるでしょう。

2つのIObservableから1回だけ値が流れる場合は、順番関係なく2つから流れたときに後続に流し始めるCombineLatestの方が良いでしょう。もし、順番が明確であればWithLatestFromでも問題ないかもしれません。


ただ、使い分けが難しいと感じたのが順番が明確ではなく、1つのIObservableのみ発火してほしい場合だと感じています。その場合は、どうにかして決まった順で流れるようにするかもう一方から値が流れて発火した場合を許容するかになるかもしれません。

参考

Rxのマーブルを自由に移動させて、オペレータに通すとどうなるか動的にわかるサイトになっているので参考になります。

また、いくつかの画像作成で使用しました。

rxmarbles.com

Unityで映画Matrixの暗号パーティクルを作る

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

始めに

つい先日「マトリックス リザレクション」のPVが公開されました。 滅茶苦茶楽しみです。


www.youtube.com


で、マトリックスと言えばあの意味の分からない文字の羅列が印象的ですが、それってパーティクルで作れない?ってことでノリと勢いで作ってみました。



プロジェクト

Unity 2020.3.7f1を使っています。

github.com



作成までの手順

私は普段全くパーティクルを触っていないので、何となくで作っています。

文字を用意する

フォトショとかイラレなどで普通は作るかもしれませんが、Googleスライドに文字列を書いてそれをスクショして文字のテクスチャを作成しました。

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


こんな感じに。

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



シェーダーで一文字ずつ描画するようにする

とても久しぶりにシェーダーを書いたので凄く雑になります。

やってることはTilingの値を一文字の大きさとして、与えられたIndexから計算してその文字を表示しています。

TextMapShader

Shader "Unlit/TextMapShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        
        _Index ("Index", int) = 0
        
        _MaxIndexX ("Max Index X", int) = 0
        _MaxIndexY ("Max Index Y", int) = 0
        
        _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"

            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;
            float _DiscardThreshold;

            int _Index;

            uniform int _MaxIndexX;
            uniform int _MaxIndexY;
            
            
            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
            {
                float letterSizeX = _MainTex_ST.x;
                float letterSizeY = _MainTex_ST.y;
                
                uint maxIndex = _MaxIndexX * _MaxIndexY;
                
                uint index = floor(_Index) % maxIndex;
                
                uint indexX = index % _MaxIndexX;
                uint indexY = index / _MaxIndexX;

                i.uv.x += letterSizeX * indexX;
                i.uv.y -= letterSizeY * indexY;
                
                fixed4 col = tex2D(_MainTex, i.uv);
                if(col.b > _DiscardThreshold)
                    discard;
                
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}


上手くいけば、InspectorのIndexをいじることで文字を切り替えることができます。

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

パーティクルの基礎作成

まず親となるパーティクルを作成します。基本的な設定は次の通りです。

  • Shapeにチェックをつけて、ShapeBoxにしてScaleをいい感じに設定
  • SubEmittersにチェックをつけて、後に作成する子のパーティクルをBirthで設定
  • Rendererのチェックを外す

この設定でパーティクルを1つの方向に出し続けるものができます。(下のgifは見やすいようRendererをチェックつけた状態にしています)

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


次に新しいパーティクルを作成し、先ほどのパーティクルの子に設定します。

パーティクルの設定は次のようになります。

  • Shapeのチェックを外す
  • Start Speedの値を小さくする
  • Rendererでテキストを出すマテリアルを設定する

あとはいい感じに値を調整します。(Start Size, Start Lifetime, etc)

出来れば次のようになります。 何となく完成が見えてきました。

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



Custom Dataを使ってParticleからシェーダーに値を渡す

Particle SystemのCustom Dataにチェックをつけて、表示する文字のIndexと色をシェーダーに渡すようにします。

また、文字をランダムに表示させたいので乱数のSeedも作成します。

子のパーティクルのCustom Dataを次のように設定します。

  • Custom1で文字のIndexと乱数のSeedを設定
  • Custom2で文字の色の変化を設定(マトリックスの映画を見るに最初の一瞬は白、そして緑)

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


次に、RendererのCustom Vertex Streamsにチェックをつけて、値を頂点シェーダーで渡してもらうよう設定します。 横の()で書かれている通り、Custom1.xyの値がTEXCOOR0.zwに入って渡されます。

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


後は渡された値を使って、シェーダーでindexを変えたり色を設定したりします。加えて、時間経過で文字が変わるようにもしました。

TextMapShader

Shader "Unlit/TextMapShader"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        
        _MaxIndexX ("Max Index X", int) = 0
        _MaxIndexY ("Max Index Y", int) = 0
        
        _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"

            struct appdata
            {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                float4 color : TEXCOORD1;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
                float index : TEXCOORD1;
                float seed : TEXCOORD2;
                float4 color : TEXCOORD3;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _DiscardThreshold;

            uniform int _MaxIndexX;
            uniform int _MaxIndexY;
            
            float GetRandomNumber(float2 texCoord, int Seed)
            {
                return frac(sin(dot(texCoord.xy, float2(12.9898, 78.233)) + Seed) * 43758.5453);
            }
            
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.index = v.uv.z;
                o.seed = v.uv.w;
                o.color = v.color;
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                float letterSizeX = _MainTex_ST.x;
                float letterSizeY = _MainTex_ST.y;
                
                uint maxIndex = _MaxIndexX * _MaxIndexY;
                
                uint time = floor(_Time.w * frac(i.seed));
                uint index = GetRandomNumber(float2(time, time + i.seed), i.seed) * 10000;
                index = index % maxIndex;
                
                uint indexX = index % _MaxIndexX;
                uint indexY = index / _MaxIndexX;

                i.uv.x += letterSizeX * indexX;
                i.uv.y -= letterSizeY * indexY;
                
                fixed4 col = tex2D(_MainTex, i.uv);
                if(col.b > _DiscardThreshold)
                    discard;
                
                UNITY_APPLY_FOG(i.fogCoord, col);
                return i.color;
            }
            ENDCG
        }
    }
}

大体上手くいけば、ほぼほぼマトリックスの暗号の動作をするようになります。

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


あとはRendererのFilpをいじってみたり、Size over Lifetimeにチェックを入れて少しずつ小さくするようにするといい感じになります。



PostProcessingでBloomをつける

パーティクルだけだとやはり味気ないので、PostProcessingを使って見ます。

まずはPostProcessingに入る前に、Main CameraのClear FlagsSolid Colorに変更にします。

あとはPostProcessingを入れて、Bloomを追加してテキトーに設定すればかなりマトリックスのオープニングっぽくなります。 (ここら辺はあまり詳しくないので、調べてみてください. . .)

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



最後に

いつも見た目にこだわったものなどを作ったりはしないので、Particle SystemだったりPostProcessingを触るいい機会となりました。

また、やってみて意外にもっぽいものができたので楽しかったです。

最後に蛇足になりますが、マトリックスに出てくる暗号のやつは実はお寿司のレシピをスキャンしてできたものらしいです。

参考

Particle Systemでシェーダーに値を渡せないか調べてた際に参考になった記事です。

styly.cc

aizu-vr.hatenablog.com

neareal.net

使わなくなったHTC ViveフルセットとViveトラッカー1つでフルトラをする話

始めに

Vive Pro EyeとValve Index Controllerを購入して、使っていたHTC Viveのデバイスが不要になりました。

ですが、調べてみたところViveコントローラーをViveトラッカーにすることが出来るようなので試してみました。


フルトラまでにやったこと

Viveコントローラーをトラッカーにする

調べてみたところこちらの記事が見つかりました。
内容としては少し古いですが、問題なくコントローラーをトラッカーに置き換えられました。

hoshigari-gamer.com


HTC ViveのHMDをドングルの代わりにする

先ほどの記事だったり、ほかの記事でもドングルはSteamコントローラレシーバーのファームウェアを書き換えるというものが主でした。

しかし、探してみたところSteamコントローラレシーバーはどこも販売していないようでした。

Steam コントローラ ワイヤレスレシーバー | PRO スチーマー


悩んだところ、こちらの記事ではHMDをドングルとして扱っていのを見つけました。

teruaki-tsubokura.com

ということで、HTC Viveに電源とUSBのみをつけてPCに接続したところ、2つトラッキングできるデバイスが増えました。

接続の際の注意点ですが、HMDとして使用するものより先に接続すると、HTC Viveの方をHMDとして認識するようなので、接続は最後にした方が良いかもしれません。


Vive トラッカー3.0を購入

あと一つでフルトラができるようになるので購入しました。

項目の内容が購入しただけだと寂しいので少しだけレビューをします。

f:id:vxd-naoshi-19961205-maro:20210830203926j:plain

トラッカーは宣伝通り、一回り小さくなっています。買う前はこの点はあまり利点にはならないかと思っていましたが、予想に反して装着した際の安定感が良くなったと感じました。

また、裏面がゴムのようになっているのでズレ防止になっています。

ただし、高さはあまり変わっていないのでトラッカーの出っ張りが大きいように感じました。


最後に個人的一番良かった変更点はUSB-TypeCになっていたところです。


装着する

準備ができたので後は装着していきます。

トラッカーにしたViveコントローラーはスリッパに結束バンドでつけました。

f:id:vxd-naoshi-19961205-maro:20210830230824j:plain


トラッカーは余ってたベルトに結束バンドでつけました。 f:id:vxd-naoshi-19961205-maro:20210830231602j:plain


最後にHTC ViveのHMDを接続してドングルの代わりを務めてもらいます。 すべてトラッキング出来たことを確認出来たら準備完了です。

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



VRCでフルトラしてみる

簡単にフルトラ出来そうだったものがVRCだったので早速試してみました。

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


最後に

新しいヘッドセットに買い替えた場合は、トラッカー1つを買うだけでフルトラが出来ることがわかりました。

また、VRChatのキャリブレーションを使うことで高精度なフルトラが可能ということも勉強になりました。今後としては自分の環境でもVRChatのようなフルトラが出来たらいいなと思ってるので、少しずつ情報収集をしていきます。

【UniRx】オペレータのSwitchについて

始めに

UniRxのオペレータでSwitchを見かけたのですが、コードの挙動を追うことが出来ていなかったので勉強してまとめてみようと思います。



Switchとは?

Switchは簡単に言いますとIObservable<T>を切り替えるオペレータです。

IObservable<IObservable<T>>の拡張メソッドで、ストリームからIObservable<T>を受け取った際にすでにIObservable<T>を購読していた場合は破棄して新しく受け取ったIObservable<T>を購読します。

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



Switchを使ったカウントスタートボタン

言葉のみの説明では全くわからなかったので、簡単なサンプルを作成してみました。

ボタンを押したら、1秒おきにカウントを流すIObservableを生成してSwitchで新しいストリームに切り替えるプログラムです。

using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class CountStartButton : MonoBehaviour
{
    [SerializeField] private Button _button;
    [SerializeField] private Text _text;
    
    void Start()
    {
        _button.OnClickAsObservable()
            .Select(_ => 
                Observable.Timer(dueTime:
                    TimeSpan.FromSeconds(0), period:
                    TimeSpan.FromSeconds(1f))
                )
            .Switch()
            .Subscribe(count => _text.text = count.ToString())
            .AddTo(this);
    }
}


実行結果は次のようになりました。 ボタンを押すとカウントがスタートします。

ボタンを押すごとに、前のストリームが破棄されて新しいものに切り替わるため、カウントが0からスタートし直すことがわかります。

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



画像読み込みサンプル

次に、SelectMany, Switchを使用して画像読み込みサンプルを作成してみました。

内容はInputFieldにPathを入力して、それをもとにSpriteを非同期でロードするものです。また、ロードの際は1秒ほど遅延を入れています。

画像は次の3つを用意して入力しやすいようファイル名をそれぞれA, B, Cとしました。

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


SelectManyを使ったスクリプト

始めにSelectManyを使ったスクリプトを作成します。

InputFieldの文字が変わるたびに、スプライトをロードします。

using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class LoadWithSelectMany : MonoBehaviour
{
    [SerializeField] private Image _image;
    [SerializeField] private float _delayTime;
    [SerializeField] private InputField _inputField; 
    
    void Start()
    {
        _inputField.OnValueChangedAsObservable()
            .SelectMany(spritePath => LoadSprite(spritePath).ToObservable())
            .Subscribe(sprite => _image.sprite = sprite)
            .AddTo(this);
    }

    private async UniTask<Sprite> LoadSprite(string spritePath)
    {
        var sprite = await Resources.LoadAsync<Sprite>(spritePath);
        await UniTask.Delay(TimeSpan.FromSeconds(_delayTime));
        return (sprite as Sprite);
    }
}



結果は次のようになりました。

"A"を入力した後すぐに消して"B"を入力した場合、一度Aの画像が表示されてデフォルトのImageになり、最後にBの画像が表示されました。

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



Switchに書き換える

次にSelectManyの部分をSwtichに置き換えたプログラムを用意しました。

    void Start()
    {
        _inputField.OnValueChangedAsObservable()
            .Select(spritePath => LoadSprite(spritePath).ToObservable())
            .Switch()
            .Subscribe(sprite => _image.sprite = sprite)
            .AddTo(this);
    }


結果は次のようになりました。

"A"を入力した後にすぐ消してに"B"を入力した場合、Aの画像やデフォルトのImageは表示されず"B"が表示されました。

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


ただ今回の画像読み込みのサンプルはToObservable()を使っているため、UniTask自体は裏で動いたままなので本当はCancellationTokenでキャンセルした方が良いでしょう。(参考: UniTaskをCancellationTokenを指定しながらToObservableするメモ - Qiita

 

2つを比較して

両方で共通している個所はInputFieldに変更があった場合に新しいIObservableを生成しているところですが、SelectManyは生成されたIObservableをすべて並列で実行しています。

反対に、Switchでは画像読み込み中でも新しいIObservableが来た場合は新しいものを購読するので前のものは無視されます。 



参考

reactivex.io

light11.hatenadiary.com

neue.cc