Aizu-Progressive xr Lab blog

会津大学のVR部であるA-PxLの部員が持ち回りで投稿していくブログです。部員がそれぞれVRに関する出来事やVRにちなんだことについて学んだことを書いていきます。

連絡はサークルTwitterのDMへお願いします。
「面白法人カヤックVR分室」としても活動しています。詳細はこちら

細分割曲面を使って蛇口のひねりを作る

学部3年モデリング班の杉山です。

今回は新入生向けチュートリアル作成の過程で制作した「蛇口のひねり」を用いてBlenderの便利なモディファイア「細分割曲面」を紹介していきます。

細分割曲面とは

「細分割曲面」は、低ポリゴンなメッシュオブジェクトを、滑らかな曲面になるようにポリゴン分割化(細分化)する事ができるモディファイアーです。(引用→

blender-cg.net

細分割曲面の使いどころ

さっそく蛇口を作成していきます。下図はキューブを押しだしなどして作りました。

f:id:aizu-vr:20190915172841p:plain

ここにさっそく細分割曲面適用してみます。すると・・・

f:id:aizu-vr:20190915173754p:plain

こうなります。簡単にきれいな曲面になります。

しかしただ適用するだけでは滑らかになってほしくない部分まで滑らかにされてしまっています。そういう時には、さきほどのリンク先の記事にある「クリース」を使うのもいいですが、「エッジーループを挿入する」ことでも解決することができます。

f:id:aizu-vr:20190915174920p:plain

エッジループを追加し、ひねりのキャップ部分がはっきりしました。

また細分割曲面を使うとメッシュ数が跳ね上がるのである程度分割数を増やした後はスムーズシェードに切り替えましょう。

完成品

 スムーズシェード&適当なマテリアルをくっつけて先ほどのキューブの塊が

f:id:aizu-vr:20190915170832p:plain

綺麗な蛇口のひねりになりました!

曲面の多い物のモデリングには欠かせない細分割曲面、この記事がひとつ参考になれば幸いです。

ShurikenのCustomVetexStreamsでパーティクル毎のデータをシェーダーに渡す

こんにちは、学部3年ゲーム部VFX班の森口です。
今日はShurikenの設定項目であるCustomVetexStreamsを使ってシェーダーに値を渡す方法を紹介したいと思います。
目次

初めに

今回の記事は技術評論社さんから出版されているUnityゲームエフェクトマスターガイドを主に参考にしており作成するエフェクトの一部にその本で使われているアセットを用いています。そのあたりの説明は本題とは異なるため省略させていただきますのでご了承ください。 この本は非常に参考になる本ですのでエフェクト制作に興味がある方はぜひ購入してみてください。 Unityゲームエフェクトマスターガイドはこちら www.amazon.co.jp

CustomVertexStreamsとは

CustomVetexStreamsとはUnity5.5から追加された機能で簡単に言うとユーザが定義した各パーティクルが持つデータをシェーダーに渡すことができる機能です。
詳しい機能などは公式リファレンスを参照してください。

docs.unity3d.com

例えばテクスチャをUVスクロールするようなエフェクトを作ろうとしたときにUVスクロールを実装したシェーダーをそのまま適応するとすべてのパーティクルが同じようにスクロールしてしまいあまり見栄えのいいものではなくなってしまいます。

そこでCustomVertexStreamsを使うとパーティクルごとに違う値を割り当て、それぞれが違う動きをしてくれるようになります。

今回はこの機能を使って雷のチャージエフェクトを作成していきたいと思います。

CustomVetexStreamsからデータをシェーダーに送る

では実際に実装していきます。
まずはShurikenのRendererモジュールを見てください。そこにCustomVetexStreamsのチェックボックスがありますのでチェックを入れてください。すると項目が増えたかと思います。

f:id:aizu-vr:20190827171933p:plain
Rendererモジュール
おそらく何も設定していない状態ですと上の画像よりも薄いグレーの中の項目が少ないと思います。その中の項目がシェーダーに送られるデータたちです。上の画像にある項目の中で今回主に使うのはCustom1とCustom2の二つになります。 これらの項目は右下にある「+」をクリックしCustom->Custom1(または2).xyzwをクリックすることで追加できます。
これでデータを送る準備はできたのでシェーダー側にうつってどのように受け取るか見ていきましょう。
まずは頂点シェーダーとフラグメントシェーダーに入力として渡す構造体です。

struct appdata {//頂点シェーダーへの入力
         float4 vertex : POSITION;
         float4 uv : TEXCOORD0;
         fixed4 color : COLOR;
         float4 custom1 : TEXCOORD1;
         fixed4 custom2 : TEXCOORD2;
};

struct v2f {//フラグメントシェーダーへの入力
         float4 vertex : SV_POSITION;
         fixed4 color : COLOR;
         float2 uv : TEXCOORD0;
         float4 custom1 : TEXCOORD1;
         float4 custom2 : TEXCOORD2;
                
};

custom1、custom2という変数に先ほどのものが入る形になります。 今回はCustom1にUVスクロールのスピードとEmissionを、Custom2にパーティクルのベースカラーにかけ合わせる色情報を持たせています。
次に頂点シェーダーとフラグメントシェーダーです

v2f vert(appdata v){
         v2f o;
         o.vertex = UnityObjectToClipPos(v.vertex);
         o.uv = TRANSFORM_TEX(v.uv, _MainTex);
         o.color = v.color;
         o.custom1 = v.custom1;
         o.custom2 = v.custom2;
         return o;
}
            
fixed4 frag(v2f i) : SV_TARGET{
         i.uv.y = i.uv.y+_Time.y*i.custom1.y;
         //y軸方向のUVスクロール
         fixed4 c = tex2D(_MainTex, i.uv)*i.color*i.custom2*i.custom1.w;
         //パーティクルの色にcustom2で指定した色を掛けてEmissionを掛けている
         return c;
}

今回はあらかじめ雷用のテクスチャを作成してから使用しているのでy軸方向のUVスクロールのみですがプロシージャルに作成している場合はx軸方向のスクロールも入れることでさらにいい感じになると思います。
できたシェーダーの全体はこちらです

Shader "Custom/Lightning"
{
    Properties
    {
        _MainTex ("MainTex", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent"}
        Blend SrcAlpha OneMinusSrcAlpha
        Cull off
        LOD 100

        Pass{
            CGPROGRAM
            // Physically based Standard lighting model, and enable shadows on all light types
            #pragma vertex vert 
            #pragma fragment frag 
            #pragma multi_compile_fog
            #include "UnityCG.cginc"

            // Use shader model 3.0 target, to get nicer looking lighting
            #pragma target 3.0

            

            struct appdata {
                float4 vertex : POSITION;
                float4 uv : TEXCOORD0;
                fixed4 color : COLOR;
                float4 custom1 : TEXCOORD1;
                fixed4 custom2 : TEXCOORD2;
            };

            struct v2f{
                float4 vertex : SV_POSITION;
                fixed4 color : COLOR;
                float2 uv : TEXCOORD0;
                float4 custom1 : TEXCOORD1;
                float4 custom2 : TEXCOORD2;
                
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert(appdata v){
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                o.color = v.color;
                o.custom1 = v.custom1;
                o.custom2 = v.custom2;
                return o;
            }
            
            fixed4 frag(v2f i) : SV_TARGET{
                i.uv.y = i.uv.y+_Time.y*i.custom1.y;
                fixed4 c = tex2D(_MainTex, i.uv)*i.color*i.custom2*i.custom1.w;
                return c;
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
}

Shurikenの設定

次にShurikenのほうの設定を行います。Mainモジュールなどは皆さんの好みに変えてくれればと思います。 ちなみに私はこんな感じに設定してます。

f:id:aizu-vr:20190827175947p:plain
Mainモジュール
f:id:aizu-vr:20190827180331p:plain
Emissionモジュール&ColorOverLifetimeモジュール
特に肝心なのが次の設定項目でRendererモジュールの一つ上にあるCustomDataというモジュールにチェックを入れます。このモジュールに設定した項目が先ほどのCustom1,2に対応しています。なのでここの値を変化させることでパーティクルごとのUVスクロールのスピードを変えたりすることができるわけです。
f:id:aizu-vr:20190827182138p:plain
CustomData モジュール
Custom1はVector形でx~wに値を割り振ってあります。今回はyの値がUVスクロールのy軸方向の速度でwがEmissionの値です(別に割り振りには特に意味はないので自由に割り振ってください)。 今回はスクロールスピードをRandomBetweenTwoCurvesにしているのでかなり速度がばらけるようになっています。 後は先ほど作成したシェーダーから作成したマテリアルを割り当ててRenderModeをMeshにして平面を少しねじったようなメッシュを割り当てて中央のコアを作成すれば完成!

最後のほうが少し雑になってしまいましたが使うメッシュはシンプルな長方形でもきれいに見えます。

最後に

いかがだったでしょうか。まだまだシェーダーは勉強中なので今回テクスチャの作成は事前に行いましたがもう少し勉強してより良いエフェクトを作れるようになりたいです。 今回の記事はここまでにしたいと思います。ご覧いただきありがとうございました。

参考ページ

goisagi-517.hatenablog.com

docs.unity3d.com

シェーダーでルーン文字書いてみた

こんにちは, 今月からA-PxL代表になりました学部2年の木村です. これからA-PxL をもっとxR技術に触れたり, 開発や勉強などしやすい場にすることを目指していきたいと思います!

最近シェーダーの勉強をして遊んでおり, 今回はルーン文字を表示するシェーダーを書いたのでその紹介をしたいと思います.

ルーン文字とは

ルーン文字は「呪術や儀式に用いられた神秘的な文字」と紹介されることもあるが、実際には日常の目的で使われており、ルーン文字で記された書簡や荷札なども多数残されている。呪術にも用いられていたが、それが盛んに行われるようになったのは、むしろラテン文字が普及しルーン文字が古めかしくいかにも神秘的に感じられるようになった時代に入ってからである。

Wikipediaより

ルーン文字はずっと昔に使われていた文字で, ラテン文字が普及した頃には呪術などにも使われるようになった文字とのことです. アニメやゲームでの魔法演出などによく使われており, 昨年の部内でチーム開発した「DESIGN」という作品でも使っていました.

https://youtu.be/UG9Lq4oFoQk?t=142

木片などにナイフで刻みつけて表記していたようなので, 直線を組み合わせた字形となっていてシェーダーでも書きやすそうです.

実装

前述の通り, ルーン文字は直線を組み合わせた字形のため, 長方形を組み合わせればよさそうです. レイマーチングで使っていた距離関数で2Dの絵も描けるようなのでやってみました. 距離関数とは, 相手との距離を返す関数のことです. こちらに詳しく解説されていましたのでご参照ください.

https://happy-runes.jimdo.com/%E3%83%AB%E3%83%BC%E3%83%B3%E6%96%87%E5%AD%97%E4%B8%80%E8%A6%A7-%E6%84%8F%E5%91%B3/

この表の左上の「フェオ(Feoh)」という文字を書いてみます.

float dRoundRect(float2 pos, float2 size){
    float2 d = abs(pos) - size;
    return length(max(d, .0))
                    + min(max(d.x, d.y), .0);
}

float dFeoh(float2 pos, float size){
    pos *= 100. / size;
    float width = .1;
    pos.x += 1.;
    float rect1 = dRoundRect(pos, float2(width, 10));
    return rect1;
}

float3 map(float2 uv){
    float2 fPos = frac(uv) * 2 -1;
    return dFeoh(fPos, 5);
}

fixed4 frag (v2f i) : SV_Target{
     fixed4 col = 1; 
     col.rgb = step(.5, map(i.uv));
     if(col.r >= 1. && col.g >= 1. && col.b >= 1.){
         discard;
     }
     return col;
}

↑は丸みのある長方形の距離関数をサイズ調整しただけです.

static const float PI = 3.141592;

float2 rotate(float2 pos, float angle){
     angle = angle * PI / 180.;
     float2 a = normalize(angle);
     float s = sin(angle);
     float c = cos(angle);
     return float2(pos.x * c - pos.y * s,
                   pos.x * s + pos.y * c);
}

// 略

float dFeoh(float2 pos, float size){
    pos *= 100. / size;
    float width = .1;
    pos.x += 1.;
    float rect1 = dRoundRect(pos, float2(width, 10));
    float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.));
    return rect2;
}

次は長方形を回転させたものです. 変更のない関数などは省略しました.
これで長方形を好きな角度で表示することができるようになったので, 次は組み合わせてみます.

float opUnion(float d1, float d2){
    return min(d1, d2);
}

// 略

float dFeoh(float2 pos, float size){
    pos *= 100. / size;
    float width = .1;
    pos.x += 1.;
    float rect1 = dRoundRect(pos, float2(width, 10));
    float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.));
    float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 1.8), 45.), float2(width, 4.));

    return opUnion(rect1, opUnion(rect2, rect3));
}

opUnionは和集合の演算をしています. このように, 和集合や差集合などを使って距離関数の合成ができます.

あとは同様に24個分のルーン文字を作成するだけです.

// ----------------- Rune -------------------------
            // フェオ
            float dFeoh(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;
                pos.x += 1.;
                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.8), 45.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 1.8), 45.), float2(width, 4.));

                return opUnion(rect1, opUnion(rect2, rect3));
            }

            // ウル
            float dUr(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x + 5., pos.y + .5), float2(width, 9.5));
                float rect2 = dRoundRect(float2(pos.x - 5., pos.y + 2.), float2(width, 8));
                float rect3 = dRoundRect(rotate(float2(pos.x, pos.y - 7.5), 106), float2(width, 5.3));

                return opUnion(rect1, opUnion(rect2, rect3));
            }

            // ソーン
            float dThorn(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y)), 130), float2(width, 7));

                return opUnion(rect1, rect2);
            }

            // アンスール
            float dAnsur(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 7.), -45.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 2.), -45.), float2(width, 4.));

                return opUnion(rect1, opUnion(rect2, rect3));                
            }

            // ラド
            float dRad(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y - 5.45)), 130), float2(width, 7));
                float rect3 = dRoundRect(rotate(float2(pos.x - 4, pos.y + 3.), 130), float2(width, 5.));

                return opUnion(rect1, opUnion(rect2, rect3));
            }

            // ケン
            float dKen(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(rotate(float2(pos.x, abs(pos.y)), 45.), float2(width, 10));

                return rect1;

            }

            float dGeofu(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(rotate(float2(pos.x, pos.y), 45.), float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x, -pos.y), 45.), float2(width, 10));

                return opUnion(rect1, rect2);
            }

            float dWynn(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 5.5, abs(pos.y - 5.45)), 130), float2(width, 7));

                return opUnion(rect1, rect2);
            }

            float dHagall(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x + 6., pos.y + .5), float2(width, 9.5));
                float rect2 = dRoundRect(float2(pos.x - 6., pos.y + .5), float2(width, 9.5));
                float rect3 = dRoundRect(rotate(float2(pos.x, pos.y), 120), float2(width, 6.5));

                return opUnion(rect1, opUnion(rect2, rect3));
            }

            float dNied(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x, pos.y - 1.), 120), float2(width, 6.5));
                
                return opUnion(rect1, rect2);
            }

            float dIs(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect = dRoundRect(pos, float2(width, 10));
                return rect;
            }

            float dJara(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(rotate(float2(pos.x + 6., abs(pos.y - 2.)), 50.), float2(width, 7.));
                float rect2 = dRoundRect(rotate(float2(pos.x - 6., abs(pos.y + 2.)), -50.), float2(width, 7.));

                return opUnion(rect1, rect2);
            }

            float dYr(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(pos, float2(width, 10));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3.1, pos.y - 7.35), -50.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x + 3.1, pos.y + 7.35), -50.), float2(width, 4.));

                return opUnion(rect1, opUnion(rect2, rect3));

            }

            float dPeorth(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                pos.x += 1.;
                float rect1 = dRoundRect(float2(pos.x + 5., pos.y), float2(width, 8.));
                float rect2 = dRoundRect(rotate(float2(abs(pos.x - 1.4), pos.y - 3.15), 53.), float2(width, 8.));
                float rect3 = dRoundRect(rotate(float2(abs(pos.x - 1.4), pos.y + 3.15), -53.), float2(width, 8.));
                
                return opUnion(rect1, opUnion(rect2, rect3));
            }

            float dEolh(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.));
                float rect2 = dRoundRect(rotate(float2(abs(pos.x), pos.y - 2.), 45.), float2(width, 8.));

                return opUnion(rect1, rect2);
            }

            float dSigel(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(rotate(float2(pos.x - .7, pos.y + 4.), 53.), float2(width, 4.));
                float rect2 = dRoundRect(rotate(float2(pos.x - .7, pos.y - .7), -53.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x - .7, pos.y - 5.5), 53.), float2(width, 4.));

                return opUnion(rect1, opUnion(rect2, rect3));
            }

            float dTir(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 7.5), -53.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x + 3.3, pos.y - 7.5), 53.), float2(width, 4.));
                
                return opUnion(rect1, opUnion(rect2, rect3));
            }

            float dBeorc(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;
                pos.x += 2.;

                float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 10.));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 7.5), -53.), float2(width, 4.));
                float rect3 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y - 2.7), 53.), float2(width, 4.));
                float rect4 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y + 7.5), 53.), float2(width, 4.));
                float rect5 = dRoundRect(rotate(float2(pos.x - 3.3, pos.y + 2.7), -53.), float2(width, 4.));
                
                return opUnion(rect1, opUnion(rect2, opUnion(rect3, opUnion(rect4, rect5))));
            }

            float dEoh(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x - 7, pos.y + .5), float2(width, 9.5));
                float rect2 = dRoundRect(float2(pos.x + 7., pos.y + .5), float2(width, 9.5));
                float rect3 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 6.4), 54.), float2(width, 4.3));
                float rect4 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 6.4), -54.), float2(width, 4.3));

                return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4)));
            }

            float dMann(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x - 7, pos.y + .5), float2(width, 9.5));
                float rect2 = dRoundRect(float2(pos.x + 7., pos.y + .5), float2(width, 9.5));
                float rect3 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 6.9), 60.), float2(width, 4.1));
                float rect4 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 6.9), -60.), float2(width, 4.1));
                float rect5 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 3.), 60.), float2(width, 4.1));
                float rect6 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 3.), -60.), float2(width, 4.1));

                return opUnion(rect1, opUnion(rect2, opUnion(rect3, opUnion(rect4, opUnion(rect5, rect6)))));
            }

            float dLagu(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;
                pos.x += 1.;

                float rect1 = dRoundRect(float2(pos.x, pos.y), float2(width, 9.5));
                float rect2 = dRoundRect(rotate(float2(pos.x - 3., pos.y - 6.5), -45.), float2(width, 4.3));

                return opUnion(rect1, rect2);
            }

            float dIng(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y + 3.5), 45.), float2(width, 5.));
                float rect2 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y + 3.5), -45.), float2(width, 5.));
                float rect3 = dRoundRect(rotate(float2(pos.x + 3.5, pos.y - 3.5), 45.), float2(width, 5.));
                float rect4 = dRoundRect(rotate(float2(pos.x - 3.5, pos.y - 3.5), -45.), float2(width, 5.));
                
                return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4)));
            }

            float dOthel(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(rotate(float2(pos.x - .5, pos.y + 3.5), 45.), float2(width, 6.));
                float rect2 = dRoundRect(rotate(float2(pos.x + .5, pos.y + 3.5), -45.), float2(width, 6.));
                float rect3 = dRoundRect(rotate(float2(pos.x + .5, pos.y - 3.3), 45.), float2(width, 3.5));
                float rect4 = dRoundRect(rotate(float2(pos.x - .5, pos.y - 3.3), -45.), float2(width, 3.5));
                
                return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4)));
            }

            float dDaeg(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;

                float rect1 = dRoundRect(float2(pos.x - 7, pos.y), float2(width, 9.5));
                float rect2 = dRoundRect(float2(pos.x + 7., pos.y), float2(width, 9.5));
                float rect3 = dRoundRect(rotate(float2(pos.x, pos.y ), 36.5), float2(width, 11.7));
                float rect4 = dRoundRect(rotate(float2(pos.x, pos.y ), -36.5), float2(width, 11.7));

                return opUnion(rect1, opUnion(rect2, opUnion(rect3, rect4)));
            }

            float dBlank(float2 pos, float size){
                pos *= 100. / size;
                float width = .1;
                return 1.;
            }

// ---------------------------------------

ルーン文字の距離関数だけで300行近くなってしまいました. しんどい...

これでとりあえずすべての文字がシェーダーで書けるようにはなりましたが, 他にもいろいろ詰め込んで最終的にできたものはこちら↓です

https://github.com/bigdra50/Shaders/blob/master/ShaderLab/Rune.shader

最後に

これをGPUパーティクルにしたものを記事にしたくて実装中でしたが, まだまだ時間がかかりそうだったのでここまでにしました.
だいぶ無理やりな実装になってる気がするのでもっといいやり方あれば教えて頂けると嬉しいです...

参考

ルーン文字関連 https://happy-runes.jimdo.com/%E3%83%AB%E3%83%BC%E3%83%B3%E6%96%87%E5%AD%97%E4%B8%80%E8%A6%A7-%E6%84%8F%E5%91%B3/ https://ja.wikipedia.org/wiki/%E3%83%AB%E3%83%BC%E3%83%B3%E6%96%87%E5%AD%97 距離関数 https://qiita.com/7CIT/items/fe33b9b341b9918b6c3d#%E8%A7%92%E4%B8%B8%E9%95%B7%E6%96%B9%E5%BD%A2 http://www.iquilezles.org/www/articles/distfunctions2d/distfunctions2d.htm https://www.slideshare.net/shohosoda9/threejs-58238484 図形や文字の描画 http://karanokan.info/2019/03/31/post-2465/ http://www.shaderslab.com/demo-75---matrix-pattern.html http://wordpress.notargs.com/blog/blog/2015/08/21/pixel-shaderglsl-sandbox%e3%81%a7%e3%83%87%e3%82%b8%e3%82%bf%e3%83%ab%e3%81%aa%e3%82%bf%e3%82%a4%e3%83%9e%e3%83%bc%e3%82%92%e4%bd%9c%e3%82%8b/

nfcpyを使って学生証から学籍番号を読み取る

こんにちは。A-PxL元代表の橋本です。先日の集会を最後に代表を引退することになりました。1年と半年、振り返ってみれば楽しいこと、辛いこと、うれしいことが色々ありましたが、どれもいい思い出たちばかりです。

さて、今週は私がブログ当番なので私の最近の話をしようと思います。内容は、学生証から学籍番号を読み取ったことについてです。

目次

動機

会津大では、毎年4月に新入生が入ってくると新入生のために歓迎会を開くことが通例となっており、この機会を使って上級生は新入生に向けて自身が所属するサークルの勧誘活動を行なっております。各サークルはブースを設けており、そこに新入生が行って内容を聞くということをしてます。そして、気になったサークルがあれば、そこのブースにある 紙の名簿に自身の学籍番号と名前を書き込みます。この名簿に書かれた学籍番号を使ってサークル関係者は学内メールを通してサークルの説明会や体験会についての連絡を新入生にするわけですね。

問題なのは、紙の名簿だということです。 メールを送信する際には、紙に書かれた学籍番号をPCに手打ちする作業が出てきます。書き込んでくれた学生が少ない場合は問題ないのですが、多いとそれだけ苦労するというわけです。2018年度の新歓では私たちの部にはなんと50名もの新入生が名簿に記入してくれました。嬉しかったのですが、その後の50人分の学籍番号の手打ち作業はとても辛かったですね(笑)

ということで、この手打ち作業をなくすべく、何かシステムを作らねば! というのが今回このシステムを作ろうと思ったきっかけです。

要件

個人的にNFCリーダーを使った何かをすることに興味がありました。そこで、学生証から学籍番号を読み取ることを考えました。また、読み取ったデータはスプレッドシートに書き込まれるようにするのがいいかと考え、読み込んだ学籍番号はGoogle Spread Sheetに書き込まれるようにしました。さらに、せっかくうちの部ではUnityを使ったアプリ開発を行なっているので、どうせなら読み込まれた学籍番号をUnity上で少し演出を加えて表示させようと思い、この実装も行うことにしました。(最後の1つはシステム的には蛇足ですけどね(笑))

f:id:aizu-vr:20190802165529p:plain

実は、この作品は私が大学で受けていた授業の最終プロジェクトとして作ったものです。そのプロジェクトは、ラズパイを使って何かしよう! というものでした。そのためこのシステムではラズパイを用いております。

今回はICカードリーダー周りについて書いていきます。

NFCFelicaについて

NFCとは、Near Field Communicationの略で、かざすことデータ交換をするために規定された近距離無線通信技術です。そして、FelicaNFCの規格をもとにしてSONYが作った規格です。特徴として、Felicaは通信速度が速く、NFCの2倍ほどと言われています。そのこともあって、日本国内では主にFelicaが使われることが多いです。SuicaPasmo, nanacoカード、WAONカードなどはすべてFelicaです。

詳しくはこちらの記事をお読みください。

[PASMO] FeliCa から情報を吸い出してみる - FeliCaの仕様編 [Android][Kotlin] - Qiita

学籍番号を読み取る

学籍番号を読み取るには読み取るための装置が必要です。手元になかったので買いました。ネットで色々調べた結果、以下の装置を購入しました。

ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380

ソニー SONY 非接触ICカードリーダー/ライター PaSoRi RC-S380

意外と安く手に入るものなんですね。

さて、準備も整ったので、早速読み取ってみましょう。まずは環境を整えます。以下の記事を参考にして整えてみてください。

RaspberryPiで!SONYのPaSoRi(RC-S380)で(NFC)Felica情報を読み取る! - KOKENSHAの技術ブログ

次に、カードのどこの部分に学籍番号が書かれているかを知らなければなりません。そこで、ダンプしてカードの中のどこに学籍番号情報があるかを確認します。

コードはこちらの方のものを使わせていただきました。

Raspberry Pi 3にPaSoRiを接続してSuicaカードをダンプする | TomoSoft

会津大の学生証をダンプした結果

System 809E (unknown)
Area 0000--FFFE
  Area 1000--1FFF
    Random Service 64: write with key (0x1008)
    Random Service 68: write with key (0x1108)
    Random Service 72: write with key (0x1208)
    Random Service 76: write with key (0x1308)
    Random Service 128: write with key (0x2008)
    Area 3000--3FFF
    Random Service 192: write with key & read w/o key (0x3008 0x300B)
     0000: 31 30 30 30 31 32 35 3X 3X 3X 3X 30 30 30 30 30 |1000125XXXX00000|
System FE00 (Common Area)
Area 0000--FFFE
  Area 1A80--1AFF
    Area 1A81--1AFF
      Random Service 106: write with key & read w/o key (0x1A88 0x1A8B)
       0000: 30 31 30 30 31 32 35 3X 3X 3X 3X 00 00 00 30 31 |0100125XXX...01|
       0001: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
       0002: 30 3X 30 3X 3X 3X 3X 3X 3X 30 3X 3X 30 3X 30 3X |0X0XX0XXX0XX0X01|
       0003: 3X 30 3X 3X 30 3X 3X 31 3X 30 30 30 30 30 3X XX |X0XX0XXXX00000XX|
      Area 1BXX--1BXX
      Area 1XXX--1BXX
        Random Service 108: write with key & read with key (0x1BXX 0x1BXX)
        Area 1XXX--1BXX
        Area 1BXX--1B7F
          Random Service 109: write with key & read with key (0xXX48 0xXX4A)
          Area 4XX0--42XX
          Area 4XX1--42XX
            Random Service 267: write with key & read with key (0xXXC8 0x4XXX)
            Area X3X0--4X3F
            Area X30X--4X3F
              Random Service 268: write with key & read with key (0x43XX 0x4XXX)
              Area XXXX--43XX
              Area 43XX--4XX7
                Random Service 269: write with key & read w/o key (0x43XX 0xXX4B)
                 0000: 00 XX 00 00 XX 00 XX 00 XX 00 XX 00 XX 00 XX XX |................|
                 *     00 XX 00 XX 00 XX 00 XX 00 XX 00 XX 00 00 XX XX |................|
                 000X: 0X 0X 00 XX 00 00 XX 00 XX 00 XX XX 00 00 00 00 |................|

データ保護のためところどころにXを入れています。

どうやら、システム809Eのサービスコード0x300Bに学籍番号らしきものがあるようですね。この値を使って学籍番号を読み取ってみたいと思います。Pythonコードを書いていきます。

こちらの記事を参考にしました。

nfcpy で複数の System Code を持つ NFC タグを扱う方法 - uchan note

#!/usr/bin/env python
# -*- coding: utf-8 -*-
  
import binascii
import nfc
import time

# 学生証のサービスコード
service_code = 0x300B

def on_connect_nfc(tag):
  # タグのIDなどを出力する
  # print tag
  
  if isinstance(tag, nfc.tag.tt3.Type3Tag):
    try:
        sc = nfc.tag.tt3.ServiceCode(service_code >> 6 ,service_code & 0x3f)
        bc = nfc.tag.tt3.BlockCode(0,service=0)
        data = tag.read_without_encryption([sc],[bc])
        sid =  "s" + str(data[4:11])
        print sid
    except Exception as e:
      print "error: %s" % e
  else:
    print "error: tag isn't Type3Tag"
  
def main():
  clf = nfc.ContactlessFrontend('usb')
  while True:
    clf.connect(rdwr={'on-connect': on_connect_nfc})
    time.sleep(3)
  
if __name__ == "__main__":
  main()

このコードを使うことで会津大の学生証から学籍番号を読み取ることができます。このプログラムでは3秒毎に学生証の学籍番号を読み取ってコンソールに表示させています。

f:id:aizu-vr:20190802184316p:plain

まとめ

最初は難しいのかなと思っていたのですが、思ったほど難しくはなかったですね。先駆者様の方々のおかげです。今回はラズパイを使ったこともありPythonを使用しましたが、調べてみたところ、C#にもNFCを扱うライブラリがあるそうなので、時間があったらやってみたいと思ってます。

来年の新歓はこれで少しは楽になるといいなぁ〜

会津大学VR部の部員が持ち回りで投稿していくブログです。特にテーマに縛りを設けずに書いていきます!