凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

ノードベースのエディタで Shader をリアルタイムに編集して色々な質感を簡単に作れる Shdaer Forge を使ってみた

はじめに

本記事はUnity アセット真夏のアドベントカレンダー 2014 Summer!の 9 日目の記事になります。前回は id:baba_s さんによる 【Unity】Editor Console Pro でゲーム開発効率化 - コガネブログ でした。

Unity では、シェーダによって様々な質感や効果を加えた表現がされています。

しかしながらシェーダを書くには色々と知識が必要で、思い通りのものを作れるようになるまでかなり時間がかかるイメージです。また、知識がついた後でも色々な物体に応じて数多くのシェーダを書くのは大変です。

そこで、本エントリでは、ノードベースビジュアルプログラミングでシェーダをグラフィカルに作成できる Shader Forge という Asset の紹介をしたいと思います。

今年 4 月に行われた Unite Japan 2014 でも @tsubaki_t1 さんが紹介されていました。

$80 / PC の有料のアセットですが、公式といわれても疑いようのないクオリティで、ノードを組みわせて Diffuse や Specular、Normal を始めとして、Refraction の調整や、Vertex の変形、Tessellation に至るまで様々なパラメータを簡単に調整し、それらをリアルタイムでプレビューしてみることが出来ます。

f:id:hecomi:20140809123705p:plain

一通りの雰囲気を概観できるようにまとめてみました。

ドキュメント

  • Shader Forge
    • 具体的にどういったノードが利用できるのかについてアニメーション GIF と共にまとめられています。
  • Shader Forge Wiki - Shader Forge Wiki
    • どういうノードの接続の仕方をすれば何が出来るのかカテゴリ別にアニメーション付きでまとまっています。

チュートリアル

公式で動画によるチュートリアルが3つあがっています。

Shader Forge - Making a basic shader

Shader Forge - Vertex color blending & UV tiling

Shader Forge - Custom Blinn-Phong

最初の動画あたりを見ると、Shader Forge のイメージが掴めると思います。

取り敢えず使ってみる

f:id:hecomi:20140805204500p:plain

Window > Shader Forge からウィンドウを開き、「New Shader」を選択します。

f:id:hecomi:20140805204605p:plain

すると初期画面が表示されます。最初から表示されているこの「Main」ノードに色々な情報を入力する経路を作ることで、最終的な見栄えを完成させるわけです。まずは色をつけるために右側の検索バーに「Color」と打ち込むと出てくる「Color」ノードをドラッグ&ドロップして適当な色を指定、RGB の出力を「Main」の入力につなぎます。

f:id:hecomi:20140805204724p:plain

シェーダがリアルタイムにコンパイルされて赤く表示されました!この要領で「Texture2D」ノードを生成して同じく線を伸ばせば画像が表示されます。ノードの上部で名前を変更できるので、ここでは「MainTex」と名前をつけてみました。

f:id:hecomi:20140805205259p:plain

この要領でポチポチノードを配置していけば、チュートリアルのムービーのように法線マップと色を指定できるシェーダの出来上がりです。

f:id:hecomi:20140805210729p:plain

マテリアルの作成

ここまでではシェーダを作っただけで、Unity ではシェーダはマテリアルに指定して使います。Shader Forge のエディタ上では画像を指定してプレビューを見ていましたが、実際にはシェーダはプレースホルダしか有することが出来ません(色は持てます)。そこで適当なマテリアルを作成、そこに先ほど作成したシェーダを適用し、プレースホルダを自分で埋める必要があります。シェーダを適用した直後はこんな感じになります。

f:id:hecomi:20140805211151p:plain

そしてテクスチャを同じものを指定すれば、プレビューで見ていたものと同じ表現になるわけです。

f:id:hecomi:20140805211658p:plain

考えを変えればテクスチャを差し替えた別のマテリアルにも同じシェーダが適用できるわけです、便利ですね。でもプレースホルダを埋めてくれたマテリアルも一緒に作ってくれたらいいのになぁ...、とも思います。

キーボードショートカット

ちょっと操作についても触れておきます。

右クリックで各ノードを選択できるコンテキストメニューが表示されます。

f:id:hecomi:20140805205803p:plain

また、アルファベット押下(しっぱなし) -> スクロール -> クリックでもノードを挿入できます。

f:id:hecomi:20140805205838p:plain

また Ctrl + C / Ctrl + V でのコピーや Ctrl + D での複製も出来ます。しばらくハマってたのですが、不要になったノードを消去するには「⌘ + X」(Ctrl + X)のみで、delete キーは効かないようです。

サンプルを見てみる

何が出来るか雰囲気が掴めたところでサンプルを見てみましょう。サンプルは Shader Forge ディレクトリ直下に Example Scene という名前でシーンが入っています。

f:id:hecomi:20140805223425p:plain

見たいオブジェクトを選択して Inspector から「Open Shader in Shader Forge」を選択するとどういったノードの接続をしているかを見ることが出来ます。以下は物理ベースシェーダを作るためのノードの接続例です。

f:id:hecomi:20140805223924p:plain

知識不足でちょっと説明はできないですが、ノードで接続されていると何をしているかとても見やすいです。またパラメタをリアルタイムにいじりながら結果を見れるので、シェーダのイメージを掴みながら勉強する上でも役に立ちそうです。

出力されるコード

Shader Forge で作成したシェーダは簡単に読めるコードとして出力されます。ちょっと長いですが書き出してみます。

// Shader created with Shader Forge Beta 0.36 
// Shader Forge (c) Joachim Holmer - http://www.acegikmo.com/shaderforge/
// Note: Manually altering this data may prevent you from opening it in Shader Forge
/*SF_DATA;ver:0.36;sub:START;pass:START;ps:flbk:,lico:1,lgpr:1,nrmq:1,limd:1,uamb:True,mssp:True,lmpd:False,lprd:False,enco:False,frtr:True,vitr:True,dbil:False,rmgx:True,rpth:0,hqsc:True,hqlp:False,tesm:0,blpr:0,bsrc:0,bdst:0,culm:0,dpts:2,wrdp:True,ufog:True,aust:True,igpj:False,qofs:0,qpre:1,rntp:1,fgom:False,fgoc:False,fgod:False,fgor:False,fgmd:0,fgcr:0.5,fgcg:0.5,fgcb:0.5,fgca:1,fgde:0.01,fgrn:0,fgrf:300,ofsf:0,ofsu:0,f2p0:False;n:type:ShaderForge.SFN_Final,id:1,x:32512,y:32689|diff-58-OUT,normal-52-RGB;n:type:ShaderForge.SFN_Tex2d,id:3,x:32996,y:32639,ptlb:MainTex,ptin:_MainTex,tex:b66bceaf0cc0ace4e9bdc92f14bba709,ntxv:0,isnm:False;n:type:ShaderForge.SFN_Tex2d,id:52,x:32822,y:32835,ptlb:Normal,ptin:_Normal,tex:bbab0a6f7bae9cf42bf057d8ee2755f6,ntxv:3,isnm:True;n:type:ShaderForge.SFN_Multiply,id:58,x:32791,y:32656|A-3-RGB,B-59-RGB;n:type:ShaderForge.SFN_Color,id:59,x:32996,y:32835,ptlb:MainColor,ptin:_MainColor,glob:False,c1:0.7573529,c2:0.7051217,c3:0,c4:1;proporder:3-52-59;pass:END;sub:END;*/

Shader "Shader Forge/MyFirstShader" {
    Properties {
        _MainTex ("MainTex", 2D) = "white" {}
        _Normal ("Normal", 2D) = "bump" {}
        _MainColor ("MainColor", Color) = (0.7573529,0.7051217,0,1)
    }
    SubShader {
        Tags {
            "RenderType"="Opaque"
        }
        Pass {
            Name "ForwardBase"
            Tags {
                "LightMode"="ForwardBase"
            }
            
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define UNITY_PASS_FORWARDBASE
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #pragma multi_compile_fwdbase_fullshadows
            #pragma exclude_renderers xbox360 ps3 flash d3d11_9x 
            #pragma target 3.0
            uniform float4 _LightColor0;
            uniform sampler2D _MainTex; uniform float4 _MainTex_ST;
            uniform sampler2D _Normal; uniform float4 _Normal_ST;
            uniform float4 _MainColor;
            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 texcoord0 : TEXCOORD0;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float4 posWorld : TEXCOORD1;
                float3 normalDir : TEXCOORD2;
                float3 tangentDir : TEXCOORD3;
                float3 binormalDir : TEXCOORD4;
                LIGHTING_COORDS(5,6)
            };
            VertexOutput vert (VertexInput v) {
                VertexOutput o;
                o.uv0 = v.texcoord0;
                o.normalDir = mul(float4(v.normal,0), _World2Object).xyz;
                o.tangentDir = normalize( mul( _Object2World, float4( v.tangent.xyz, 0.0 ) ).xyz );
                o.binormalDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
                o.posWorld = mul(_Object2World, v.vertex);
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                TRANSFER_VERTEX_TO_FRAGMENT(o)
                return o;
            }
            fixed4 frag(VertexOutput i) : COLOR {
                i.normalDir = normalize(i.normalDir);
                float3x3 tangentTransform = float3x3( i.tangentDir, i.binormalDir, i.normalDir);
                float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
/////// Normals:
                float2 node_63 = i.uv0;
                float3 normalLocal = UnpackNormal(tex2D(_Normal,TRANSFORM_TEX(node_63.rg, _Normal))).rgb;
                float3 normalDirection =  normalize(mul( normalLocal, tangentTransform )); // Perturbed normals
                float3 lightDirection = normalize(_WorldSpaceLightPos0.xyz);
////// Lighting:
                float attenuation = LIGHT_ATTENUATION(i);
                float3 attenColor = attenuation * _LightColor0.xyz;
/////// Diffuse:
                float NdotL = dot( normalDirection, lightDirection );
                float3 diffuse = max( 0.0, NdotL) * attenColor + UNITY_LIGHTMODEL_AMBIENT.rgb;
                float3 finalColor = 0;
                float3 diffuseLight = diffuse;
                finalColor += diffuseLight * (tex2D(_MainTex,TRANSFORM_TEX(node_63.rg, _MainTex)).rgb*_MainColor.rgb);
/// Final Color:
                return fixed4(finalColor,1);
            }
            ENDCG
        }
        Pass {
            Name "ForwardAdd"
            Tags {
                "LightMode"="ForwardAdd"
            }
            Blend One One
            
            
            Fog { Color (0,0,0,0) }
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define UNITY_PASS_FORWARDADD
            #include "UnityCG.cginc"
            #include "AutoLight.cginc"
            #pragma multi_compile_fwdadd_fullshadows
            #pragma exclude_renderers xbox360 ps3 flash d3d11_9x 
            #pragma target 3.0
            uniform float4 _LightColor0;
            uniform sampler2D _MainTex; uniform float4 _MainTex_ST;
            uniform sampler2D _Normal; uniform float4 _Normal_ST;
            uniform float4 _MainColor;
            struct VertexInput {
                float4 vertex : POSITION;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float2 texcoord0 : TEXCOORD0;
            };
            struct VertexOutput {
                float4 pos : SV_POSITION;
                float2 uv0 : TEXCOORD0;
                float4 posWorld : TEXCOORD1;
                float3 normalDir : TEXCOORD2;
                float3 tangentDir : TEXCOORD3;
                float3 binormalDir : TEXCOORD4;
                LIGHTING_COORDS(5,6)
            };
            VertexOutput vert (VertexInput v) {
                VertexOutput o;
                o.uv0 = v.texcoord0;
                o.normalDir = mul(float4(v.normal,0), _World2Object).xyz;
                o.tangentDir = normalize( mul( _Object2World, float4( v.tangent.xyz, 0.0 ) ).xyz );
                o.binormalDir = normalize(cross(o.normalDir, o.tangentDir) * v.tangent.w);
                o.posWorld = mul(_Object2World, v.vertex);
                o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
                TRANSFER_VERTEX_TO_FRAGMENT(o)
                return o;
            }
            fixed4 frag(VertexOutput i) : COLOR {
                i.normalDir = normalize(i.normalDir);
                float3x3 tangentTransform = float3x3( i.tangentDir, i.binormalDir, i.normalDir);
                float3 viewDirection = normalize(_WorldSpaceCameraPos.xyz - i.posWorld.xyz);
/////// Normals:
                float2 node_64 = i.uv0;
                float3 normalLocal = UnpackNormal(tex2D(_Normal,TRANSFORM_TEX(node_64.rg, _Normal))).rgb;
                float3 normalDirection =  normalize(mul( normalLocal, tangentTransform )); // Perturbed normals
                float3 lightDirection = normalize(lerp(_WorldSpaceLightPos0.xyz, _WorldSpaceLightPos0.xyz - i.posWorld.xyz,_WorldSpaceLightPos0.w));
////// Lighting:
                float attenuation = LIGHT_ATTENUATION(i);
                float3 attenColor = attenuation * _LightColor0.xyz;
/////// Diffuse:
                float NdotL = dot( normalDirection, lightDirection );
                float3 diffuse = max( 0.0, NdotL) * attenColor;
                float3 finalColor = 0;
                float3 diffuseLight = diffuse;
                finalColor += diffuseLight * (tex2D(_MainTex,TRANSFORM_TEX(node_64.rg, _MainTex)).rgb*_MainColor.rgb);
/// Final Color:
                return fixed4(finalColor * 1,0);
            }
            ENDCG
        }
    }
    FallBack "Diffuse"
    CustomEditor "ShaderForgeMaterialInspector"
}

4 行目のコメントにノードの位置や接続などの情報が格納されているようです。

外から変数を指定できるようにプレースホルダとなるノードは Properties に書きだされます。先ほど指定した名前が変数名として与えられています。

Properties {
    _MainTex ("MainTex", 2D) = "white" {}
    _Normal ("Normal", 2D) = "bump" {}
    _MainColor ("MainColor", Color) = (0.7573529,0.7051217,0,1)
}

全体を見てみると、メインライトや環境光用に ForwardBase、追加のライト用に ForwardAdd の 2 つの Pass が作成されています。それぞれ内部でノードの流れに沿う用に VertexInputVertexOutput が定義され、頂点シェーダの vert とフラグメントシェーダの frag が定義されています。シェーダ自体はそれほど長いコードでも読みにくいコードでもないので吐き出されたシェーダを調整する、みたいなこともそれほど難しくないように思われます。

既存のシェーダを読み込む

Shader Forge は通常の Unity のシェーダを書き出す仕組みなので、逆にシェーダを読み込むことも出来るようです。が、試しに、Unity に付属のトゥーンシェーダを読み込んでみようとしたところ、うまく行きませんでした...。

f:id:hecomi:20140805212240p:plain

「Replace with Shader Forge shader」というボタンがあるのでクリックして、指定したシェーダのノードが出てきたら格好良かったのですが。

コードの利用

ビジュアルプログラミングの欠点は簡単なことをやりたいだけなのに複雑にノードを結ばなければならず見た目が煩雑になってしまう点だと思います。例えば時間に応じて明滅する以下の様なシェーダを作ったとします。

f:id:hecomi:20140805221150p:plain

結構ぐちゃぐちゃしますね...。そこで Shader Forge には「Code」ノードが用意されています。ここではノードの値を入力とした関数が Cg 言語をそのまま書けます。

f:id:hecomi:20140805221458p:plain

単純なノードが大分すっきりしました(本当は _SinTime.x を使おうと思ったのですが、プレビューでアニメーションされなかったので「Time」ノードを入力にしてます)。適宜こういったノードを利用することでシンプルな構成を保てる気がします。

Skyshop との連携

Skyshop をインポートして Examples.zip および ShaderForgeExtension.zip を解凍します。

f:id:hecomi:20140807125634p:plain

すると、Shader Forge 内で Skyshop のノードが使えるようになります。

f:id:hecomi:20140807125745p:plain

f:id:hecomi:20140807125804p:plain

超綺麗です。

その他

Shader Forge - Real-time spherical area lights

カスタムライトのサンプル動画です。

類似の Asset

同じようにノードベースでシェーダを記述できる Unity のアセットに、無料の Strumpy Shader Editor があります。優良なこともあり、Shader Forge の方がよりグラフィカルに且つ色々なことができる高機能なものになっている印象です。

Unity オフィシャルのノードベースエディタ?

Shader Forge について調べている最中、Unity の中の人の Tim Cooper さんが開発しているノードベースのエディタの動画を見かけました。

Shader Editor Beta 4 from Tim Cooper on Vimeo.

2010、2011 頃の動画投稿だったので今は開発停止しているのでしょうか...。

おわりに

Unity 5 より汎用的に使える物理ベースシェーダの導入がされますが、より細かい調整が必要な際はやはり自前のシェーダが必要になってくると思います。そういった際に Shader Forge を使うと、直にシェーダを書くよりも、より少ない知識で短時間に所望の質感が作れると思います。また、グラフィカルにシェーダが表現されることにより、中で何が起きているのか把握しやすくなり、学習コストも下がります。ちょっとお高めですが、それだけの価値はあると思いますので、導入してみてはいかがでしょうか。

次回は、Poto7 さんによる「1日で作るカジュアルゲーム!FlappyXXXXを作ろう」になります。

Oculus Rift DK2 が届いたので遊んでみた

はじめに

ついに Oculus Rift DK2 (Developer Kit 2) を手に入れました!

f:id:hecomi:20140803002344j:plain

f:id:hecomi:20140803002350j:plain

f:id:hecomi:20140803002031j:plain

DK2 は 2014/7 時点で全世界で 45,000 個もの注文(DK1 は 2014/3 時点で 60,000 個)の注文があったとのことです。私は開発用と布教用で 2 個買いました。

公式の動画を含め、いくつか動画を見るといろいろとイメージが掴めると思います。

外観など筐体の特徴については @kawauso3 さんによる週刊アスPlus の以下の記事がとてもわかり易く、中身については iFixit による分解レポートもあがっているため、合わせてみると特徴がよくわかると思います。

自分の知識を整理するためと、ハンズオン時の感想を記録・共有するためにエントリをまとめてみました。

特徴 - DK1 との違い

今回のアップデートのポイントはいくつかあります。

すでにご存じの方も多いと思いますが下記にまとめておきます。

ポジショナルトラッキングによる自然な動き

DK1 では回転しか取れなかったため下から覗き込むといった動作が実現できていませんでした。DK2 からは 60 Hz の近赤外線 CMOS センサ(赤外線カメラ)をプレイヤーの前方に配置することで、HMD 側に仕込まれた赤外線 LED を検知、そこから位置を割り出すといった手法で、位置のトラッキングを実現しています。

f:id:hecomi:20140803130301j:plain

f:id:hecomi:20140803130311j:plain

以下の動画が大変わかり易いです。

OLED 液晶による残像感のない高コントラストな映像

DK1 では 1280 x 800(640 x 800 / eye) だったのに対し、DK2 では 1920 x 1080 (960 x 1080 / eye) になりました。またパネルも LCD から高速で自発光型な OLED になり、よりきれい(黒が黒く見えるなど)で残像感のない映像が見れるようになりました。ディスプレイは冒頭の iFixit 記事によると、 Samsung の Galaxy Note 3 のパネル(60 Hz)をオーバークロックして 75 Hz で利用しているようです。

実際に装着してみると、DK1 の時に気になっていた網目(ドットとドットの隙間)が見えてしまう効果もかなり低減されていて、より自然な映像になっていました。また、色収差を補正するためにスクリーン上の映像は RGB がずれたような絵になっています。

f:id:hecomi:20140803142528p:plain

ちょっとずれているとこの RGB のズレが目立ってしまいますが、横のダイヤルを回して液晶までの距離を調節したり、バンドで装着位置をしっかり調整すれば気にならなくなり、より鮮明な画になります。こういったいくつかの対応が SDK 側でもなされていて、例えばディストーション用に大きく(2364x1461)描画するなどの対応がされています。

f:id:hecomi:20140803154607p:plain

これらハード・ソフト両面からのきめ細やかな施策により、よりきれいな VR 映像が実現されています。

複数の遅延対策による自然なヘッドトラッキング

ジャイロ、加速度、磁気センサによる 1000 Hz なヘッドトラッキングは前回と同じですが、今回はレイテンシーテスターが新しく入りました。レイテンシーテスターは以前 DK1 用にスタンドアロンで売られていたものが、今回ビルトインされた形になっています。

Oculus Rift では予測補間をすることにより遅延を吸収していますが、この補間は実際に目に届くまでの時間を知らないと正確にはなりません。そこで光学式の本テスターにより実際に目に液晶の映像が入るまでの時間をリアルタイムに計測することでより自然な動きを実現しています。

遅延に関しては公式でスライドも上がっています。

コントロールボックスの排除

なくなりました。ケーブルは HDMI とヘッドトラッキング用のケーブル 2 本がまとめられて 1 本になって本体から出ています。代わりに、カメラとのシンクケーブルが 1 本増えました。あと本体に USB 口が 1 つついているのが面白いです。色々と連携デバイスが出てくると面白いですね。

ちょっと重くなった

DK1 は 395g だったのに対し、DK2 は 453g と重くなっています。確かに装着した感じ少し重い印象も受けますが、今回からコントロールボックスが排除されたことを考慮すればトータルでは相当な軽量化

ちょっと高くなった

DK1 は $300 + 送料 $50 でしたが、今回は $350 + 送料 $75 になりました。それでも安い...。

セットアップについて

エヴァンジェリストの @GOROman さんをはじめとする有志の方々による素晴らしい情報がまとめられています。今回は単純な映像出力ではないため、色々とインストールに手こずることが多い(私も動かすまでに時間かかりました...)ので、導入前に一度目を通しておくことを強くお勧めします。

必要とされるスペックについて

解像度が上がったことも影響し、DK1 の時よりもより高スペックが求められています。しかしながら Untiy での開発に関しては、アンチエイリアスをオフにすることで、ある程度パワーのない PC でも動くようです。

設定について

Oculus Configration Utility

f:id:hecomi:20140803165818p:plain

複数のユーザ設定が保存できます。中でも重要なのが Advanced にある IPD(瞳孔間距離)の設定です。設定すると自然に立体視ができるようになります。

f:id:hecomi:20140803170229p:plain

これは @yuujii さんがスクショ付きでまとめてくださっているので以下のページをご参照ください。

Rift Display Mode

f:id:hecomi:20140803170353p:plain

「Direct HMD Access from Apps」モードと、「Extend Desktop to the HMD」モードという 2 つのモードがあります。

Direct HMD Access from Apps モード

f:id:hecomi:20140803164926p:plain

Windows にディスプレイとして認識されずに、直接 HMD へ描画を行うモードです。上記のようにどういった画面が表示されているかを別ウィンドウで見ることができます。後述の Extend モードと比較してレイテンシが低減されるモードになり、前述のレイテンシテスターも動作する模様です。可能であればこちらを使いましょう。

Extend Desktop to the HMD モード

f:id:hecomi:20140803165611p:plain

従来通り、Windows にディスプレイ(縦として認識)として認識されます。

アプリケーション

以下のリンクに DK2 で遊べるソフトウェアがまとまっています。

いろいろ試した中からいくつかピックアップしてみました。

Demo Scene

f:id:hecomi:20140803171852p:plain

Configuration Utility から「Show Demo Scene」ボタンを押すと起動します。机の上にいろんなオブジェクトがあって DK2 の綺麗さとポジトラの威力を最初に体験するのに相応しいデモになっています。思わず机の上のものを取ろうとして、あれ、手がない、となる感覚はとても不思議です。

Oculus World Demo

Oculus SDK for Windows に含まれています。

最初はこんな感想をいだきました。

が、SDK 付属のデモは軽量化のために色々とエフェクトが切ってあるためチープに見えてしまうようです(@yando さんに教えていただきました)。

そこで、同サイトの Unity 4 Pro Integration でビルドをしたところ、色々なオブジェクトが動いたりともろもろのクオリティが上がり、「あー、異国来たんじゃー」感が出ました。

BLAST BUSTER 2

@yasei_no_otoko さんによる Leap Motion にも対応したシューティングゲーム。Perilous Dimension という名で DK1 時代有名でした。きれいな宝石を Leap Motion がない場合は頭の向きでロックオン、ある場合は 10 本の指を使ってロックオンして壊していくゲームです。頭の向きだけで操作できるのがすごい楽です。ホーミングレーザーがとても綺麗でぶっ壊すのが爽快です。

すわこちゃん Cubic for Oculus Rift

※ 動画は DK1 時のもの

f:id:hecomi:20140805012428p:plain

@rodonjohn さんによるジャンピング立体弾幕ゲームのすわこちゃん Cubic の待望の DK2 版です!ゲームとして一番完成している Oculus Rift ゲームだと思います。DK2 のきれいな画面&ポジトラ付きでいろんな角度から楽に見れるようになってプレイしやすくなりました。こんなにすごいものが無料でできるのは驚きです...。

最初、エラー(「動作を停止しました」で終了)で起動しなかったのですが、@rodonjohn さんに色々と教えていただき、結論としては私のパソコンに DirectX のランタイムが入っていなかったというのが原因でした。

Totoro Bus Stop Scene

著作権的にはいいのか気になりますが、あー、アニメの世界が現実だったらこんな感じになるんだろうなぁ...、と感動しました。動画を見ずにプレイするのをおすすめします。

Mikulus DK2

f:id:hecomi:20140805013642p:plain

そして @GOROman さんによる FHD でポジトラする MikulusDK2 版が昨日発表されました。DK2 を持った人々は新たなる世界へと旅立って行きました。

その他情報

ドキュメント
Alienware 17 での動作

私がテストしたのは Alienware 17(GeForce GTX880M)なのですが、以下の 3 点ひっかかりました。

認識しない

どういう理屈かはわかりませんが本体ケーブル接続部のカバーを外して別の HDMI を挿して PC と接続したところ認識し、以降認識するようになりました。

Extend Desktop to HMD モードの映像が変

DK2 はモニタの向きを「縦」にするのが普通なのですが、以下のように「横(反転)」でないと正常の表示されませんでした(縦画で表示される)。

f:id:hecomi:20140803162001p:plain

NVIDIA の設定

ドライバを最新にした後、以下の様な設定にしました。

f:id:hecomi:20140803162737p:plain

f:id:hecomi:20140803162725p:plain

Optimus を切るために優先するグラフィックスプロセッサを「高パフォーマンス NVIDIA プロセッサ」、垂直同期を「オン」、レンダリング前最大フレーム数を「1」にしています。

おわりに

以下の記事を書いてからおよそ 1 年しか経っていません。

進化に驚くばかりですし、この先を進んでいけば面白い未来がある気がしてなりません。私は平日は本職が立て込んでいて DK2 が受け取れなかったのですが、日本では Oculus Rift コミュニティの活気がすごいので、ようやく受け取れたあとは既にベストプラクティス的なレールが敷かれていて「楽しく使う Oculus Rift DK2」に従ってインストールすることで、それほど躓かずスムーズに体験までこぎつけられました。また、Twitter で DK2 で検索すると色々な知見をみんなつぶやいていますし、困っていれば色々と助けてくれます。私もこの一端を担えるよういろいろ頑張っていきたいと思います。

以下総括:

SerialPort または Uniduino を使った Unity と Arduino を連携させる方法調べてみた

はじめに

UnityArduino を連携させて色々なセンサアクチュエータを扱うことができるようになると、センサの値のビジュアライズだけに留まらず、3D モデルやゲームと連動した動きが色々と簡単に実現できるようになったり、逆にゲームの中に連動したことを実際の世界で簡単に表現したり出来ます。そこで、Unity と Arduino をつなげる方法として、無料で出来る System.IO.Ports.SerialPort を使った方法と、有料ですが簡単に Arduino と接続できるアセットの Uniduino、2つについて調べてみましたのでご紹介します。

環境

SerialPort から自前で読み取る(無料)

Player Settings から API Compatibility Level を .NET 2.0 Subset から .NET 2.0 へと変更すると、System.IO.Ports.SerialPort クラスが扱えるようになります。

f:id:hecomi:20140728021904p:plain

SerialPort クラスには様々なメンバやイベントが用意されており、例えば DataReceived イベントを利用するとシリアル通信でやってきたメッセージを受け取ることが出来ます。

SerialPort port = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);
port.DataReceived += new SerialDataReceivedEventHandler(OnDataReceived);
port.Open();

しかしながら、Unity では DataReceived イベントをはじめとする幾つかのメンバ関数が動作せず、利用することが出来ません。

ただし SerialPort.Read()Update() 内で呼んでメッセージを読み取ることは出来るようです。ただ、これは重い処理なので、Update() やコルーチンで使うと Unity のフレームレートが低下してしまう点や、そもそも 60 fps でしかシリアルからのメッセージを読み取れない(センサはもっと高速)といった問題点が生じます。そこで、スレッドを切って、そちらで値を読み取ってやることが必要になります。

前置きが長くなりましたが、コードを見て行きましょう。ここでは加速度センサの KXR94-2050 で姿勢情報を取得(読み取り)と L チカ(書き込み)をするコードになっています。回路の詳細は下記サイト様を参考にしたのでそちらをご参照ください。

デモ

Test.ino

Arduino 側に焼くスケッチです。加速度センサの値から角度を計算、また LED の ON/OFF を切り替えられるようになっています。

namespace {
  const int AVERAGE_NUM = 10;
  const int BASE_X      = 530;
  const int BASE_Y      = 519;
  const int BASE_Z      = 545;
}
  
void setup()
{
  Serial.begin(9600);
  pinMode(13, OUTPUT);
}

void readAccelerometer()
{
  int x = 0, y = 0, z = 0;
  for (int i = 0; i < AVERAGE_NUM; ++i) {
    x += analogRead(0);
    y += analogRead(1);
    z += analogRead(2);
  }
  x /= AVERAGE_NUM;
  y /= AVERAGE_NUM;
  z /= AVERAGE_NUM;
  
  const int angleX = atan2(x - BASE_X, z - BASE_Z) / PI * 180;
  const int angleY = atan2(y - BASE_Y, z - BASE_Z) / PI * 180; 

  Serial.print(angleX);
  Serial.print("\t");
  Serial.print(angleY);
  Serial.println("");
}

void setLed()
{
  if ( Serial.available() ) {
    char mode = Serial.read();
    switch (mode) {
      case '0' : digitalWrite(13, LOW);  break;
      case '1' : digitalWrite(13, HIGH); break;
    }
  }
}

void loop()
{
  readAccelerometer();
  setLed();
}

SerialHandler.cs

Unity 側でシリアル通信でやってきたメッセージを読み取ったり、書き込んだりする部分です。デリゲートでシリアル通信の受信部を別の場所で処理できるようになっています。

using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System.Threading;

public class SerialHandler : MonoBehaviour
{
    public delegate void SerialDataReceivedEventHandler(string message);
    public event SerialDataReceivedEventHandler OnDataReceived;

    public string portName = "/dev/tty.usbmodem1421";
    public int baudRate    = 9600;

    private SerialPort serialPort_;
    private Thread thread_;
    private bool isRunning_ = false;

    private string message_;
    private bool isNewMessageReceived_ = false;

    void Awake()
    {
        Open();
    }

    void Update()
    {
        if (isNewMessageReceived_) {
            OnDataReceived(message_);
        }
    }

    void OnDestroy()
    {
        Close();
    }

    private void Open()
    {
        serialPort_ = new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One);
        serialPort_.Open();

        isRunning_ = true;

        thread_ = new Thread(Read);
        thread_.Start();
    }

    private void Close()
    {
        isRunning_ = false;

        if (thread_ != null && thread_.IsAlive) {
            thread_.Join();
        }

        if (serialPort_ != null && serialPort_.IsOpen) {
            serialPort_.Close();
            serialPort_.Dispose();
        }
    }

    private void Read()
    {
        while (isRunning_ && serialPort_ != null && serialPort_.IsOpen) {
            try {
                if (serialPort_.BytesToRead > 0) {
                    message_ = serialPort_.ReadLine();
                    isNewMessageReceived_ = true;
                }
            } catch (System.Exception e) {
                Debug.LogWarning(e.Message);
            }
        }
    }

    public void Write(string message)
    {
        try {
            serialPort_.Write(message);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}

RotateByAccelerometer.cs

シリアル通信で読み込んだ加速センサの値を利用してオブジェクトを回転するコードです。

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class RotateByAccelerometer : MonoBehaviour
{
    public SerialHandler serialHandler;

    private List<Vector3> angleCache = new List<Vector3>();
    public int angleCacheNum = 10;
    public Vector3 angle {
        private set {
            angleCache.Add(value);
            if (angleCache.Count > angleCacheNum) {
                angleCache.RemoveAt(0);
            }
        }
        get {
            if (angleCache.Count > 0) {
                var sum = Vector3.zero;
                angleCache.ForEach(angle => { sum += angle; });
                return sum / angleCache.Count;
            } else {
                return Vector3.zero;
            }
        }
    }

    void Start()
    {
        serialHandler.OnDataReceived += OnDataReceived;
    }

    void Update()
    {
        transform.rotation = Quaternion.Euler(angle);
    }

    void OnDataReceived(string message)
    {
        var data = message.Split(
                new string[]{"\t"}, System.StringSplitOptions.None);
        if (data.Length < 2) return;

        try {
            var angleX = float.Parse(data[0]);
            var angleY = float.Parse(data[1]);
            angle = new Vector3(angleX, 0, angleY);
        } catch (System.Exception e) {
            Debug.LogWarning(e.Message);
        }
    }
}

LedController.cs

Unity から Arduino 側にメッセージを送って LED の ON/OFF を制御するコードです。

using UnityEngine;
using System.Collections;

public class LedController : MonoBehaviour
{
    public SerialHandler serialHandler;

    void Update()
    {
        if ( Input.GetKeyDown(KeyCode.A) ) {
            serialHandler.Write("0");
        }
        if ( Input.GetKeyDown(KeyCode.S) ) {
            serialHandler.Write("1");
        }
    }
}

上記コードをひな形として色々と改造すればだいたいのことが出来ると思います。

Uniduino を利用(有料)

Uniduino は Unity から ArduinoArduino 言語ライクに扱うことの出来る有料のアセット($30)です。

既に先人の方々がブログで解説されていますので、概念だけ簡単に紹介します。

Arduino へは Firmata(ふぁるまーた)という PC と Arduino 間で色々とやり取りできるプロトコルを実装したファームウェアを焼きます。これは Arduino のスケッチに予め用意されています。

f:id:hecomi:20140727193703p:plain

Firmata を通じた Arduino とのやり取りに関しては、Processing(Java)や oF(C++)をはじめ、Ruby や Node.js まで様々な言語の実装があります。

これを利用して、ピンの設定なども含めて PC 側の実装のみで簡単にアクチュエータの制御やセンサの値の取得が可能です。具体的にコードを見てみます。

BlinkyLight.cs

using UnityEngine;
using System.Collections;
using Uniduino;

public class BlinkyLight : MonoBehaviour
{
    private Arduino arduino_;

    void Start()
    {
        // Arduino クラスのインスタンスを取得
        arduino_ = Arduino.global;

        // Arduino の設定
        arduino_.Setup(ConfigurePins);

        // コルーチンで定期的な処理を行う
        StartCoroutine(BlinkLoop());
    }

    void ConfigurePins()
    {
        // 13 番 pin を OUTPUT に設定
        arduino_.pinMode(13, PinMode.OUTPUT);
    }

    IEnumerator BlinkLoop()
    {
        // 1 秒ごとに HIGH / LOW を切り変える
        while (true) {
            arduino_.digitalWrite(13, Arduino.HIGH);
            yield return new WaitForSeconds(1);
            arduino_.digitalWrite(13, Arduino.LOW);
            yield return new WaitForSeconds(1);
        }
    }
}

デモ

Arduino クラスを通じて色々な API を叩くことで、Arduino のスケッチで書くようなコード(setuploop)を Unity 側で書けるような形になっています(簡単)。「Uniduino > Prefabs > Uniduino」Prefab をシーンに配置して実行すれば、シリアルポートや Baud Rate などは自動的に設定されて、動画のように Arduino と連携できます。

センサの値の読み込みは以下のようになります。Uniduino を使わないで書いたコードと同じで、加速度センサから得た姿勢情報を利用してオブジェクトを回転するコードになっています。

Accelerometer.cs

using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using Uniduino;

public class Accelerometer : MonoBehaviour
{
    private Arduino arduino_;
    public Transform target;
    public Vector3 baseValues = Vector3.zero;
    public int rotationCacheNum_ = 10;

    private List<Vector3> rotationCache_ = new List<Vector3>();

    public Vector3 rotation {
        get {
            var sum = Vector3.zero;
            rotationCache_.ForEach(val => { sum += val; });
            return sum / rotationCache_.Count;
        }
    }

    void Start()
    {
        arduino_ = Arduino.global;
        arduino_.Setup(ConfigurePins);
    }

    void Update()
    {
        AddRotationCache(new Vector3(
            arduino_.analogRead(0),
            arduino_.analogRead(1),
            arduino_.analogRead(2)
        ));

        if (target) {
            target.rotation = Quaternion.Euler(rotation);
        }
    }

    void AddRotationCache(Vector3 pinValues)
    {
        var diff = pinValues - baseValues;
        rotationCache_.Add(new Vector3(
            Mathf.Atan2(diff.x, diff.z) / Mathf.PI * 180,
            0,
            Mathf.Atan2(diff.y, diff.z) / Mathf.PI * 180
        ));
        if (rotationCache_.Count > rotationCacheNum_) {
            rotationCache_.RemoveAt(0);
        }
    }

    void ConfigurePins()
    {
        SetAnalog(0);
        SetAnalog(1);
        SetAnalog(2);
    }

    void SetAnalog(int pin)
    {
        arduino_.pinMode(pin, PinMode.ANALOG);
        arduino_.reportAnalog(pin, 1);
    }
}

デモ

また、詳細は割愛しますが、Playmaker 用のカスタムアクションも用意してあるので、コード書かずにビジュアルプログラミングだけで色々なことも出来そうです。

おわりに

予算があり且つセンサの値の取得や単純な書き込み程度であれば断然 Uniduino を使ったほうが楽だと思います。予算がない場合や、ちょっとなにかやりたい時、Arduino 側のライブラリを利用したい時は自前でやった方が良いと思います。これでハードウェアと連動したプロトタイプやイベント用デモも簡単に作れそうです。