凹みTips

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

発売前に RICOH THETA S のライブビューを Unity でリアルタイムに全天球で見るやつ作ってみた

はじめに

GOROman さん(@GOROman)が、以下の様なつぶやきをされているのを見かけたので、その日のうちにやってみたお話です。

THETA S は 10/23 に発売予定の全天球カメラで、現行機の m15 にはなかった Dual-Fisheye USB カメラストリーミング(1280x720 15fps)と HDMI カメラストリーミング(1920x1080 30fps)が機能として加わります。これを Unity 上で見れるように、適当な UV 展開した球と、境界をアルファブレンドする適当なシェーダを作ってみました。最終的には、Nora さん(@Stereoarts)が直接 Equirectangular(正距円筒図法)でフラグメントシェーダだけで平面のメッシュに書き出すものを作ってくださったので、THETA S で全天球をしたい、という目的の場合はそちらを使用されたほうが圧倒的にクオリティ高い&便利だと思います(Skybox にも出来る!)。

本記事では、私の方法も一応備忘録という形で残しておこうと思って書いてみました。

サンプル

THETA で取れる画の例

THETA m15 の動画は円がオーバーラップするような形でしたが、THETA S からは綺麗に別れた形になります。片方の円でカバーする半球の画角は 180 度よりも若干広いです。

f:id:hecomi:20151011210559j:plain

作業の際は GOROman さんに WebCamTexture で撮ったサンプルのテクスチャを送ってもらって調整しました。

良い感じに UV を設定した球を作る

Maya LT で作業してみます。普通に球を作成すると UV はこんな感じに展開されています。

f:id:hecomi:20151011190947p:plain

これを適当な平面で UV 展開を行うと以下のようになります。

f:id:hecomi:20151011192233p:plain

あとは 半分に切って分割し、適当に動かせば OK です。

f:id:hecomi:20151011192313p:plain

f:id:hecomi:20151011192331p:plain

これで以下のようになります。

動画も上げました:

実際は境界でアルファブレンドを行いたかったので、球ではなくオーバーラップした半球を 2 個作りました。境界部分の UV は手動で適当に伸ばしてあります。

f:id:hecomi:20151011192608p:plain

なお、面はすべて法線を反転させて内側を向くように設定しています。UV の位置や大きさは後でシェーダの方で微調整できるようにするので適当です。

Unity で設定

Maya LT で作成したモデルを Unity でインポートして中心にカメラを置きます。モデルは UV の位置を微調整したりアルファブレンドできるようにシェーダを書きます。向きによって境界が前後することのないよう描画順を制御するために、半球ごとに別のシェーダにしています。

Shader "Theta/Sphere1" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _AlphaBlendTex ("Alpha Blend (RGBA)", 2D) = "white" {}
        _OffsetU ("Offset U", Range(-0.5, 0.5)) = 0 
        _OffsetV ("Offset V", Range(-0.5, 0.5)) = 0
        _ScaleU ("Scale U", Range(0.8, 1.2)) = 1
        _ScaleV ("Scale V", Range(0.8, 1.2)) = 1
        _ScaleCenterU ("Scale Center U", Range(0.0, 1.0)) = 0 
        _ScaleCenterV ("Scale Center V", Range(0.0, 1.0)) = 0
    }
    SubShader {
        Tags { "RenderType" = "Transparent" "Queue" = "Background" }
        Pass {
            Name "BASE"
            
            Blend SrcAlpha OneMinusSrcAlpha
            Lighting Off
            ZWrite Off
            
            CGPROGRAM
            #pragma vertex vert_img
            #pragma fragment frag

            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform sampler2D _AlphaBlendTex;
            uniform float _OffsetU;
            uniform float _OffsetV;
            uniform float _ScaleU;
            uniform float _ScaleV;
            uniform float _ScaleCenterU;
            uniform float _ScaleCenterV;

            float4 frag(v2f_img i) : COLOR {
                // 中心位置や大きさを微調整
                float2 uvCenter = float2(_ScaleCenterU, _ScaleCenterV);
                float2 uvOffset = float2(_OffsetU, _OffsetV);
                float2 uvScale = float2(_ScaleU, _ScaleV);
                float2 uv =  (i.uv - uvCenter) * uvScale + uvCenter + uvOffset;
                // アルファブレンド用のテクスチャを参照してアルファを調整
                float4 tex = tex2D(_MainTex, uv);
                tex.a *= pow(1.0 - tex2D(_AlphaBlendTex, i.uv).a, 2);
                return tex;
            }
            ENDCG
        }
    }
}
Shader "Theta/Sphere2" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _AlphaBlendTex ("Alpha Blend (RGBA)", 2D) = "white" {}
        _OffsetU ("Offset U", Range(-0.5, 0.5)) = 0 
        _OffsetV ("Offset V", Range(-0.5, 0.5)) = 0
        _ScaleU ("Scale U", Range(0.8, 1.2)) = 1
        _ScaleV ("Scale V", Range(0.8, 1.2)) = 1
        _ScaleCenterU ("Scale Center U", Range(0.0, 1.0)) = 0 
        _ScaleCenterV ("Scale Center V", Range(0.0, 1.0)) = 0
    }
    SubShader {
        Tags { "RenderType" = "Transparent" "Queue" = "Background+1" }
        UsePass "Theta/Sphere1/BASE"
    }
}

アルファブレンド用に以下の様な UV に合わせてアルファを調整したテクスチャを作っておきます。私は UV を postscript で吐き出してイラレで読み込んでピッタリ合うように調整しながら作りました(内側の白い円がアルファ = 1、円の周辺部で内側から外側にかけて 1 から 0 に変化、外側は使わないので適当)。

f:id:hecomi:20151011201530p:plain

後はパラメタを微調整すれば全天球の出来上がりです。

f:id:hecomi:20151011201436p:plain

f:id:hecomi:20151011205354p:plain

Equirectangular 化

頂点シェーダの変形でやってみました。

Shader "Theta/Equirectangular1" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _AlphaBlendTex ("Alpha Blend (RGBA)", 2D) = "white" {}
        _OffsetU ("Offset U", Range(-0.5, 0.5)) = 0 
        _OffsetV ("Offset V", Range(-0.5, 0.5)) = 0
        _ScaleU ("Scale U", Range(0.8, 1.2)) = 1
        _ScaleV ("Scale V", Range(0.8, 1.2)) = 1
        _ScaleCenterU ("Scale Center U", Range(0.0, 1.0)) = 0 
        _ScaleCenterV ("Scale Center V", Range(0.0, 1.0)) = 0
        _Aspect ("Aspect", Float) = 1.777777777
    }
    SubShader {
        Tags { "RenderType" = "Transparent" "Queue" = "Background" }
        Pass {
            Name "BASE"
            
            Blend SrcAlpha OneMinusSrcAlpha
            Lighting Off
            ZWrite Off
            
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #define PI 3.1415925358979

            #include "UnityCG.cginc"

            uniform sampler2D _MainTex;
            uniform sampler2D _AlphaBlendTex;
            uniform float _OffsetU;
            uniform float _OffsetV;
            uniform float _ScaleU;
            uniform float _ScaleV;
            uniform float _ScaleCenterU;
            uniform float _ScaleCenterV;
            uniform float _Aspect;
            
            struct v2f {
                float4 position : SV_POSITION;
                float2 uv       : TEXCOORD0;
            };

            v2f vert(appdata_base v) {
                float4 modelBase = mul(_Object2World, float4(0, 0, 0, 1));
                float4 modelVert = mul(_Object2World, v.vertex);
                
                float x = modelVert.x;
                float y = modelVert.y;
                float z = modelVert.z;
                
                float r = sqrt(x*x + y*y + z*z);
                x /= 2 * r;
                y /= 2 * r;
                z /= 2 * r;
                
                float latitude  = atan2(0.5, -y);
                float longitude = atan2(x, z);  
                
                float ex = longitude / (2 * PI);
                float ey = (latitude - PI / 2) / PI * 2;
                float ez = 0;
                
                ex *= _Aspect;
                
                modelVert = float4(float3(ex, ey, ez) * 2 * r, 1);

                v2f o;
                o.position = mul(UNITY_MATRIX_VP, modelVert);
                o.uv       = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord);
                return o;
            }    

            float4 frag(v2f i) : COLOR {
                float2 uvCenter = float2(_ScaleCenterU, _ScaleCenterV);
                float2 uvOffset = float2(_OffsetU, _OffsetV);
                float2 uvScale = float2(_ScaleU, _ScaleV);
                float2 uv =  (i.uv - uvCenter) * uvScale + uvCenter + uvOffset;
                float4 tex = tex2D(_MainTex, uv);
                tex.a *= pow(1.0 - tex2D(_AlphaBlendTex, i.uv).a, 2);
                return tex;
            }
            ENDCG
        }
    }
}
Shader "Theta/Equirectangular2" {
    Properties {
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _AlphaBlendTex ("Alpha Blend (RGBA)", 2D) = "white" {}
        _OffsetU ("Offset U", Range(-0.5, 0.5)) = 0 
        _OffsetV ("Offset V", Range(-0.5, 0.5)) = 0
        _ScaleU ("Scale U", Range(0.8, 1.2)) = 1
        _ScaleV ("Scale V", Range(0.8, 1.2)) = 1
        _ScaleCenterU ("Scale Center U", Range(0.0, 1.0)) = 0 
        _ScaleCenterV ("Scale Center V", Range(0.0, 1.0)) = 0
        _Aspect ("Aspect", Float) = 1.777777777
    }
    SubShader {
        Tags { "RenderType" = "Transparent" "Queue" = "Background+1" }
        UsePass "Theta/Equirectangular1/BASE"
    }
}

f:id:hecomi:20151011205744p:plain

メッシュを見てみるとこんな感じに移動されています。

f:id:hecomi:20151011205805p:plain

端のほうでポリゴンが当たらず空白ができてしまっていますね…。Nora さんのように直接フラグメントシェーダから起こせば回避できると思います。

おわりに

全天球の AR やスタビライズなど色々とテーマがありそうで面白そうです。発売したら色々遊んでみようと思います。