ランタイムで環境光の影響を反映させる方法

f:id:gounsx:20180607162417p:plain

キーワード

  • 環境光
  • 環境マップ
  • Cubemap
  • Reflection Probe

skyboxを動的に変更すると見た目がおかしくなる!?

ランタイムでskyboxを変更したところ、環境光が適切に反映されず、一部見た目がおかしい結果となる症状に遭遇しました。

今回はSkyboxによる環境光の影響を適切に反映してあげることで見た目をよくしたいと思います。

Unityにおける環境マップ

UnityではSkyboxにSkybox用のシェーダーを割り当てたマテリアルを設定してあげることで特に追加の設定は必要なく、環境光をオブジェクトに反映させてげることができます。

変化がわかりやすいように新規でStandardShaderを割り当てたマテリアルを作成し、MetallicSmoothness1にしてパチンコ玉のような見た目にします。

f:id:gounsx:20180607163612p:plain

DefaultのSkyboxではこのようにパチンコ玉にSkyboxが映り込んでいるのを確認することができます。

こちらのskyboxを違うskyboxに変更するとこういった結果が得られます。

f:id:gounsx:20180607163944p:plain

これは全天球画像から作られたskyboxで、劇場の椅子がしっかりと映り込んでいます。

このようにEditor停止中では適切な結果を得ることができるのですが、skyboxから行われる環境光の計算はランタイムでは動的に切り替わりません。

youtu.be

このskyboxの切り替え時にどういった処理が走っているかというと、環境マップとしてcubemapの生成を行います。

ランタイムで更新(cubemapの再生成)するにはそれなりのコストがかかるため、エディタ停止中には動作し、ランタイムでは動作しないというのはUnityエディタでそのコストを吸収しているためだと思われます。

試しにLighting ウィンドウのAuto Generateをオフにすると、環境光の影響が反映されなくなります。

そして、その右側にあるGenerate Lightingをクリックすると、シーン名フォルダが生成され、その中にcubemapが生成されます。 つまりこれをEditor停止中に内部的に生成しているということです。

この時生成したcubemapをskybox変更時にEnviroment ReflectionsSourceCustomに変更し、設定することで環境光の影響を受けるようにすることができます。

動的にcubemapを生成し、環境光の影響を適切に反映されるようにするには

今回はskybox変更時に、cubemapを生成し環境マップとして設定してあげる方向で修正を行いたいと思います。

cube mapの生成するにはいくつかの方法があります。

1つ目は、Reflection Probeを配置する。2つ目は、Camera.RenderToCubemap()を使用する方法です。

Reflection Probe

前者は非常に簡単でHierarchyからCreateLightReflection Probeと選択し、シーンに配置するだけです。

いくつかパラメータがあるのですが、TypeRealtimeReflesh ModeEvery frameにするとマイフレーム更新することができます。

f:id:gounsx:20180608181425p:plain

マイフレームではかなりのコストがかかるので、ReflectionProbe.RenderProbe()を呼ぶことで任意のタイミングで更新をかけることができます。

Camera.RenderToCubemap()

こちらはスクリプトを作成する必要があります。

参考までにcube mapを生成するスクリプト、生成したcube mapを設定するスクリプトを載せておきます。

RenderCubemap.cs

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

public class RenderCubemap : MonoBehaviour
{
    static public Cubemap Generate()
    {
        GameObject cubeMapCamera = new GameObject("CubemapCamera");
        Cubemap cubemap = new Cubemap(64, TextureFormat.RGBA32, true);
        var camera = cubeMapCamera.AddComponent<Camera>();
        cubeMapCamera.transform.position = Vector3.zero;
        cubeMapCamera.transform.rotation = Quaternion.identity;
        int layerMask = 0;
        camera.cullingMask = layerMask;
        camera.allowHDR = true;
        camera.RenderToCubemap(cubemap);
        DestroyImmediate(cubeMapCamera);
        RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Custom;
        RenderSettings.customReflection = cubemap;

        return cubemap;
    }
}

CubemapTester.cs

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

public class CubemapTester : MonoBehaviour
{
    [ContextMenu("Change Cubemap")]
    public void ChangeCubemap()
    {
        RenderSettings.defaultReflectionMode = UnityEngine.Rendering.DefaultReflectionMode.Custom;
        RenderSettings.customReflection = RenderCubemap.Generate();
    }
}

使い方はCubemapTester.csのChangeCubemap()を実行します。

f:id:gounsx:20180608181122p:plain

このように、Reflection ProbeもしくはCamera.RenderToCubemap()を使用し、任意のタイミングで更新をかけてあげることで動的に環境光を反映させることができるようになりました。

まとめ

cubemapの解像度や、HDRの有効無効の設定が異なるので上記の2つの方法での結果が少しずつことなっていますが、自分のプロジェクトに最適な方法を使用することをお勧めします。

見た目にかかわる部分なので、各種パラメータいじってみて理想の絵作りを追及してください!

UnityでglTFを使用する方法

f:id:gounsx:20180601192631p:plain

3Dモデルデータを用意

今回は検証用に3Dスキャナーでスキャンしたobjデータを用意。
フォルダの中には3つのファイルがあります。

- mesh.obj (3Dモデルデータ)
- mesh.mtl (マテリアルデータ)
- material0_basecolor.jpg (テクスチャデータ)

f:id:gounsx:20180601192816p:plain

Blenderの下準備

KhronosGroup製のglTF-Blender-Exporterレポジトリをダウンロードします。

github.com

\glTF-Blender-Exporter-master\scripts\addons\io_scene_gltf2Blenderアドオンディレクトに追加します。

BlenderのアドオンディレクトリはWindowsならC:\Program Files\Blender Foundation\Blender\2.79(バージョン番号)\scripts\addonsにあります。

次にBlenderを開き、アドオンを有効にします。
FileUser Preferencesと進み、Add-onsタブを選択後、検索窓に「gltf」と入力するとimport-Export: glTF 2.0 formatが出てくるので有効にします。
さらにEnable experimental glTF export settingsも有効にします。

f:id:gounsx:20180601194427p:plain f:id:gounsx:20180601194543p:plain

Blenderにobjデータをインポート

Blenderにobjデータをインポートします。

FileImportobjと進み、先ほどのobjデータを選択します。
真っ白な状態でインポートされました。
これからShaderの設定を行います。
f:id:gounsx:20180601195005p:plain

「glTF Metallic Roughness」シェーダーをリンクします。

FileLinkと進み、先ほどのglTF-Blender-Exporter-master内にあるglTF-Blender-Exporter-master\pbr_node\glTF2.blend\NodeTree\glTF Metallic Roughnessを選択します。

f:id:gounsx:20180601195730p:plain

これで「glTF Metallic Roughness」シェーダーが使用可能になります。

シェーダーの設定の前にレンダリングエンジンをCycles Renderに変更します。

f:id:gounsx:20180601200138p:plain

その後、MateriaのついているMeshを選択し、Materialの設定変更を行います。

f:id:gounsx:20180601200406p:plain

SurfaceからUse Nodesをクリックしたら、「glTF Metallic Roughness」シェーダーを選択します。

f:id:gounsx:20180601200719p:plain f:id:gounsx:20180601200733p:plain

設定項目の中からBase ColorImage Textureと進み、テクスチャファイルを選択します。

ここまで設定しても見た目は変わりませんが、ここで3Dモデルの表示モードをTextureに切り替えることで適切に設定されていることを確認することができます。

f:id:gounsx:20180601201046p:plain

glTF形式で出力

最後にglTF形式での出力を行います。
FileExportと進み、任意のディレクトを選択します。

f:id:gounsx:20180601203912p:plain

出力されたデータを見てみると、3つのデータから構成されていることがわかります。

- TestGLTF.gltf (glTFデータ)
- TestGLTF.bin (頂点データ)
- material0_basecolor.jpg (テクスチャデータ)

f:id:gounsx:20180601201631p:plain

Blenderでの作業はこれで以上です。

UnityでglTFファイルを読み込む

Unityを開き、Asset StoreからSktechfab for Unityをインポートします。

f:id:gounsx:20180601202144p:plain

もしくはsketchfabのUnityGLTFレポジトリからunitypackage形式でインストールすることも可能です。

github.com

インポートに成功すると‘Sketchfab‘タブが表示されるのでImport glTFを選択するとglTF Importerが表示されます。

Import file from diskをクリックし、先ほどのglTFファイルを選択後Importボタンをクリックすることで処理が始まります。

f:id:gounsx:20180601202951p:plain

少し待ってシーン上に3Dモデルが表示されれば成功です。

f:id:gounsx:20180601203231p:plain

作品アーカイブ

2015

Spatial Jockey

f:id:gounsx:20180505190619j:plain

vimeo.com

音楽に合わせて空間をリアルタイムに制御するHMD使用ソフト。
MIDIコントローラーを使用し、ジョッキーがリアルタイムで空間をコントロール
ローカルネットワークを構築し、複数台のHMDとの同期が可能です。
HMD装着者は音楽に合わせて変化する空間に身をゆだねる。


NOWHERE TEMPLE beta

f:id:gounsx:20180505191309j:plain f:id:gounsx:20180505191352j:plain

「Spatial Jockey」をプラットフォームに、現代オカルティズム研究者バンギ・アブドゥル氏とのコラボレーション作品。
HMDを使用し、実践魔術の儀式空間(Astral plane)と変性意識を体験するプログラム「NOWHERE TEMPLE beta」のデモンストレーションを行いました。

  • Oculus DK2
  • Unity

映像作家udocorg (鵜戸庚司) 個展「れいより40℃も高熱」360°映像コンテンツ

f:id:gounsx:20180505192605j:plain

個展開始直後に会場で360度動画の撮影を行い、スティッチ加工、編集を行いました。

  • QBiC MS-1 × 4台 (360度カメラ)
  • Gear VR

「ISLAND IS ISLANDS」 VRコンテンツ

f:id:gounsx:20180505193811j:plain f:id:gounsx:20180505193858j:plain

ファッションデザイナー・中里周子×写真家・小林健太による二人展「ISLAND IS ISLANDS」VRコンテンツ作成。
オブジェクトの3Dモデル作成にPhoto Scanの技術を使用。

  • Photo Scan
  • Oculus DK2
  • Unity

近距離通信アプリ 「AirMeet」

f:id:gounsx:20180505200946j:plain www.youtube.com

JPHACKS 2015応募作品
IBM賞受賞

2015.jphacks.com

  • iBeacon通信
  • node.js
  • Swift

2016

「ようこそ、ISETAN宇宙支店へ ~わたしたちの未来の百貨店~」VRコンテンツ

f:id:gounsx:20180505194250j:plain f:id:gounsx:20180505194258j:plain f:id:gounsx:20180505194309j:plain

ファッションブランド『ノリコナカザト』の伊勢丹TOKYO解放区企画店でのVRコンテンツ作成。

  • Oculus DK2
  • Unity

2017

ドット絵アーティスト・タカクラカズキ個展『有無ヴェルト』VRコンテンツ

f:id:gounsx:20180505194616p:plain

  • Vive Tracker × 2台
  • HTC Vive
  • Unity

chloma × STYLY HMD collection 展示/販売会 『chloma OS Umwelt』MRコンテンツ

f:id:gounsx:20180505194923p:plain www.youtube.com


2018

Microsoft HoloLensアプリケーション 「chloma x STYLY HMD collection」

f:id:gounsx:20180505195144j:plain

www.microsoft.com


Daydreamアプリケーション 「Nyoro The Snake & Seven Island」

www.youtube.com f:id:gounsx:20180505195604j:plain

  • Daydream
  • Unity

VTuber バートン

f:id:gounsx:20180505195853j:plain

www.youtube.com

第1回VTuberハッカソン応募作品
HTC VIVE賞受賞 panora.tokyo


Faceモデル作成アプリ (制作中)

f:id:gounsx:20180505200245p:plain

  • iPhoneX
  • Unity

リアルタイムIBLでARを現実空間に馴染ませる

f:id:gounsx:20180426144337p:plain

IBLとは

Image based lighting (イメージ・ベースド・ライティング)

画像から照明条件を算出して、ライティングを行う技術です。
2000年前後に登場した技術で、映画製作にも使われ、実写合成ではミラーボールを使用して高精細な全天球画像を作成します。
Unity5はBeastからEnlightenに移行したため、簡単にIBLを設定できるようになりました。

cgworld.jp

IBLの設定方法

Editor上のWindow -> Lighting -> Settingsから設定することができます。
Skybox Materialを変更するだけです。 これでSkyboxが環境光に影響を与えるようになります。

f:id:gounsx:20180425122106p:plain

試しに高精細なHDR全天球画像を配布しているHDR LabssIBL Archiveを使ってみます。

  1. Texture Shapeを2DからCubeに変更
  2. Create -> Materialを作成
  3. ShaderをSkybox/Cubemapに変更
  4. 1で変更したTextureをMaterialに設定
  5. LigtingのSettingsウィンドウのSkybox Materialに設定

結果

f:id:gounsx:20180425122931p:plain

このように、先程のSphereと比べて赤みがかった色合いになりました。
SkyboxのCubemapとして使用した画像が影響しているのがわかります。

f:id:gounsx:20180425123326j:plain

また、SphereのMaterial設定をMetalicを1、Smoothnessを1にすることでSkyboxの映り込みを確認することができます。  

f:id:gounsx:20180425123816p:plain

ARKit x IBL

さて、ここまではIBLの説明でしたが、「IBLとARKitを組み合わせて実在感をアップさせよう!」というのが今回の主題です。
現状のARコンテンツの大きな特徴は「キャラクターの実在感」だと個人的には考えています。
今までは平面ディスプレイ越しに接してきたキャラクターが、自分のいる現実世界に存在する、自分の机の上で踊っている・・・

しかし、ただそのまま現実空間に3Dモデルを表示するだけだと、3Dモデルが馴染まずにパキッった見え方になってしまいます。 そこで、今回はIBLを使用することで現実空間のライト状況を3Dモデルに反映し、キャラクターをより現実に溶け込ませてあげましょう。

大まかな流れ

  1. 現実世界の明るさに合わせて、Unity空間の明るさを調整
  2. カメラでキャプチャーした画像を基にIBLを行い、色合いの調整
  3. キャラクターが現実に馴染む!実在感が増す!

明るさ

ARKitにはUnityARAmbientというコンポーネントが用意されています。
これを使うことでカメラ越しに捉えた現実の明るさを基にUnity空間に存在するDirectional Lightのintensityを調整してくれます。
Lightコンポーネントが付いているGameObjectにアタッチするだけなので別途実装の必要はありません。

色合い (IBL)

では、先程説明したIBLをARKitと組み合わせて使用したいと思います。
先に大まかな実装の流れを説明すると

  1. UnityARVideoコンポーネントが取得しているyCbCrのテクスチャを取得する
  2. 変換行列を用いてRGBに変換する (先程の説明で言う所のダウンロードしてきたHDR画像)
  3. 極座標変換を行い、Skyboxに設定する (IBL)

注意!

今回実装するIBLはあくまで擬似的なものです。
というのも端末のカメラで取得できるイメージは単眼カメラで撮影したもので、そこからSkyboxを生成するというのは実際不可能だからです。
なんとなく現実空間の色合いをキャラクターに反映させたいというのが目標なのでカメラで撮影した画像に極座標変換を行うことで無理やり全天球画像にしてしまいます。

カメラで撮影した画像の取得

ARFakeIBL.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.iOS;

public class ARFakeIBL : MonoBehaviour
{
    [SerializeField]
    // ARKitで用意されているYUVMaterialを設定
    private Material _yuvMat;

    [SerializeField]
    // 疑似IBLスカイボックス用マテリアルとして使用
    private Material _yuvSkyboxMat;

    [SerializeField]
    private int _frameInterval;

    private int _frameCount;

    void Update()
    {
        if (_frameCount == _frameInterval)
        {
            Texture textureY = _yuvMat.GetTexture("_textureY");
            Texture textureCbCr = _yuvMat.GetTexture("_textureCbCr");

            _yuvSkyboxMat.SetTexture("_TextureY", textureY);
            _yuvSkyboxMat.SetTexture("_TextureCbCr", textureCbCr);

            DynamicGI.UpdateEnvironment();

            _frameCount = 0;
        }
        else
        {
            _frameCount++;
        }
    }
}

ARFakeIBLスクリプトを適当なGameObjectにアタッチします。
このスクリプトはARKitで使用しているYUVMaterialが持つyCbCRのテクスチャを今回自作するSkybox用シェーダーに受け渡しする役割を持ちます。
yuvSkyboxMatには後で説明するIBLSkyboxシェーダーから作成したマテリアルを設定してください。 Update内で処理は行っていますが、負荷との相談でframeIntervalを20ぐらいに設定しておくと良いと思います。

IBL用Skyboxシェーダー

IBLSkybox.shader

Shader "Skybox/IBLSkybox"
{
    Properties
    {
        _TextureY("TextureY", 2D) = "white" {}
        _TextureCbCr("TextureCbCr", 2D) = "black" {}
    }

    SubShader
    {
        Tags { "RenderType"="Background" "Queue"="Background" }

        Pass
        {
            ZWrite Off
            Cull Off
            Fog { Mode Off }

            CGPROGRAM
            #pragma fragmentoption ARB_precision_hint_fastest
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            #define PI 3.141592653589793

            struct appdata
            {
                float4 position : POSITION;
                float3 texcoord : TEXCOORD0;
            };
            
            struct v2f
            {
                float4 position : SV_POSITION;
                float3 texcoord : TEXCOORD0;
            };

            sampler2D _TextureY;
            sampler2D _TextureCbCr;

            static const float4x4 ycbcrToRGBTransform = float4x4(
                    float4(1.0, +0.0000, +1.4020, -0.7010),
                    float4(1.0, -0.3441, -0.7141, +0.5291),
                    float4(1.0, +1.7720, +0.0000, -0.8860),
                    float4(0.0, +0.0000, +0.0000, +1.0000)
                ); 

            inline float2 ToRadialCoords(float3 coords)
            {
                float3 normalizedCoords = normalize(coords);
                float latitude = acos(normalizedCoords.y);
                float longitude = atan2(normalizedCoords.z, normalizedCoords.x);
                float2 sphereCoords = float2(longitude, latitude) * float2(0.5/UNITY_PI, 1.0/UNITY_PI);
                return float2(0.5,1.0) - sphereCoords;
            }
            
            v2f vert (appdata v)
            {
                v2f o;
                o.position = UnityObjectToClipPos (v.position);
                o.texcoord = v.texcoord;
                return o;
            }
            
            half4 frag (v2f i) : COLOR
            {
                float2 uv = ToRadialCoords(i.texcoord);

                float y = tex2D(_TextureY, uv).r;
                float4 ycbcr = float4(y, tex2D(_TextureCbCr, uv).rg, 1.0);


                return mul(ycbcrToRGBTransform, ycbcr);
            }

            ENDCG
        }
    } 
}

ToRadialCoords関数でuv座標を極座標変換しています。
この関数自体はビルトインシェーダーのSkybox/Panoramicの中で行われている変換処理と同一のものです。
そして、変換された座標からyCbCrテクスチャ上の色をフェッチし、RGBに変換して返すといったシンプルなシェーダーになっています。
RGBに変換するための行列ですが、一般的に使用されるyCbCr → RGB変換のための値ではうまくいきませんでした。
なので素直にARKitで用意されているUnlit/ARCameraShader内に記述されている変換行列をそのまま使用しています。

最後にこのシェーダーから作成したマテリアルをLigting/SettingsウィンドウのSkybox Materialに設定します。

結果

youtu.be

youtu.be

現実の状況を3Dモデルに反映することで、そのキャラクターがよりその場に存在する実在感が生まれました。

How to fix Unity ARKit Remote


この記事は

zeroichi.hatenablog.jp

を英語での解説が欲しいとリクエストがあったので英語でまとめたものです。
要点だけまとめてあるので、詳しい解説はリンク先のものを見てください。


f:id:gounsx:20180403163958j:plain

It does not work properly!

www.youtube.com

What is wrong?

f:id:gounsx:20180403163746p:plain

Since the data transmitted can not be handled on the receiving side, the sending side can not send it!

This problem occurs because iPhoneX has a high resolution.

f:id:gounsx:20180404124240p:plain

How to fix

Using the MemoryStream, compress the data by DeflateStream before sending it on the sending side.

The point is, ...

  1. Compress the data on the data sending side .
  2. Decompress the data on the data receiving side.

Compress & Decompress

    /// <summary>
    /// Compress using deflate.
    /// </summary>
    /// <returns>The byte compress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteCompress(byte[] source)
    {
        using (MemoryStream ms = new MemoryStream())
        using (DeflateStream compressedDStream = new DeflateStream(ms, CompressionMode.Compress, true))
        {
            compressedDStream.Write(source, 0, source.Length);

            compressedDStream.Close();

            byte[] destination = ms.ToArray();

            Debug.Log(source.Length.ToString() + " vs " + ms.Length.ToString());

            return destination;
        }
    }

    /// <summary>
    /// Decompress using deflate.
    /// </summary>
    /// <returns>The byte decompress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteDecompress(byte[] source)
    {
        using (MemoryStream input = new MemoryStream(source))
        using (MemoryStream output = new MemoryStream())
        using (DeflateStream decompressedDstream = new DeflateStream(input, CompressionMode.Decompress))
        {
            decompressedDstream.CopyTo(output);

            byte[] destination = output.ToArray();

            Debug.Log("Decompress Size : " + output.Length);

            return destination;
        }
    }

How to use

UnityRemoteVideo.cs

public void OnPreRender()
        {
            ~ abridgement ~

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureYMsgId, YByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureYMsgId, ByteConverter.ConvertByteCompress(YByteArrayForFrame(1 - currentFrameIndex)));

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureUVMsgId, UVByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureUVMsgId, ByteConverter.ConvertByteCompress(UVByteArrayForFrame(1 - currentFrameIndex)));
            
        }

ARKitRemoteConnection.cs

void ReceiveRemoteScreenYTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;

            //remoteScreenYTex.LoadRawTextureData(mea.data);
            remoteScreenYTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenYTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetYTexure(remoteScreenYTex);
            }

        }

        void ReceiveRemoteScreenUVTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;
 
            //remoteScreenUVTex.LoadRawTextureData(mea.data);
            remoteScreenUVTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenUVTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetUVTexure(remoteScreenUVTex);
            }

        }

UnityRemoteVideo.cs and ARKitRemoteConnection.cs are scripts prepared beforehand by Unity ARKit Plugin.

The compression & expansion processing is the script that I created this time.

Caution!

In order to use CopyTo method of DeflateStream class I am using .NET Framework 4.6 instead of .NET Framework 3.5.

Building the Unity ARKit Remote scene uses .NET Framework 3.5. Please change to .NET Framework 4.6 after building and run Editor.

Run

www.youtube.com

You are now able to use it successfully.

If there is something you do not understand please comment:)

twitter.com

Unity ARKit Remoteがうまく動かなくて困った話

f:id:gounsx:20180404155114j:plain

ことの経緯

youtu.be

「毎回ビルドして実機チェックするにはビルドに時間もかかるし大変!」

「そうだプラグインに含まれていたUnity ARKit Remoteを使おう。」

「iPhoneXとMacBook Proのコネクト成功!」

「・・・おかしい。」

「なんだこれは・・・1フレーム目だけCaptureImageが表示されて、それ以降は全く更新されない・・・」

「一応特徴点は検出しているし、謎色空間のCaptureImageらしきものがオーバーレイでうっすら描画されている・・・」

「察するにEditorと実機はConnectされているし一部の情報は毎フレーム送信できているものの、CaptureImage周りの更新ができていない!」

一旦Unity Forumに投げる

Unity Forumsに投げるも救世主現れず。(2018/4/4時点)
Hi, I am in trouble by Unity ARKit Remote

自力で解決することに・・・

エラー発見

f:id:gounsx:20180403163746p:plain

つまり「送信されたデータを受信側で捌ききれていないので送信することができません!」ということ。

なので受信側が毎フレーム送信されたデータを捌き切れるように圧縮処理、展開処理を加えてあげることに。

余談

原因を発見し、これから説明する修正を行った後に気がついたのですが、iPhone7なら修正しなくてもちゃんと動きました。
これはiPhoneXの画面解像度がでかすぎるためでした。 iPhone7と比べると2倍以上の大きさです。 なのでこれから行う修正はiPhoneXを使用してUnity ARKit Remoteを使いたい人向けで、それ以外の端末でしたら修正しなくても問題なく動くと思います。

f:id:gounsx:20180404124240p:plain

修正方法

送信しているデータ

OnPreRenderで毎フレーム描画前にEditor側にデータを送信しています。

  • screenCaptureYMsg : カメラに映る映像(後述)
  • screenCaptureUVMsg : カメラに映る映像(後述)
  • updateCameraFrameMsg : カメラに関するトランスフォームとか、プロジェクション行列の情報

データの容量の削減を行うためにARKitではRGBではなくYUVを使用しています。
YUVフォーマット及び YUV<->RGB変換

とは言っても毎フレーム送信するにはまだ大きすぎるので先程の3つのデータの内、上2つのデータの圧縮を行います。

大まかな流れ

byte[] dataに入っているデータを MemoryStreamを使用して、DeflateStreamによる圧縮をしてから送信するようにします。

Deflateって?な方はまずはここを見るとなんとなくわかると思います。
Deflate (Wikipedia)

UnityRemoteVideo.csiPhoneのカメラに映る映像を取得し、byte配列に格納しているのでここでDeflateを使用し圧縮を行います。

実際にEditor側にデータを送信しているのはConnecetToEditor.cs内で、Guidを指定してPlayerConnectionクラスのSendメソッドで送信を行います。

ARKitRemoteConnection.csで送信されたデータの受信を行っています。
先程のSendメソッドの第一引数で指定したGuidにcallbackを登録するEditorConnectionクラスのRegisterメソッドを使用し、データの受信を行っています。 ここでMessageEventArgs.dataのbyte配列をDeflateを使用して展開し、圧縮前の状態に戻してあげます。

要するに

  1. データ送信側でデータの圧縮を行う
  2. データ受信側でデータの展開を行う

たったこれだけです。送信部分や受信部分のロジックはすでにUnity ARKit Pluginで用意されているので一から書く必要はありません。

圧縮・展開プログラム

 /// <summary>
    /// Compress using deflate.
    /// </summary>
    /// <returns>The byte compress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteCompress(byte[] source)
    {
        using (MemoryStream ms = new MemoryStream())
        using (DeflateStream compressedDStream = new DeflateStream(ms, CompressionMode.Compress, true))
        {
            compressedDStream.Write(source, 0, source.Length);

            compressedDStream.Close();

            byte[] destination = ms.ToArray();

            Debug.Log(source.Length.ToString() + " vs " + ms.Length.ToString());

            return destination;
        }
    }

    /// <summary>
    /// Decompress using deflate.
    /// </summary>
    /// <returns>The byte decompress.</returns>
    /// <param name="source">Source.</param>
    public static byte[] ConvertByteDecompress(byte[] source)
    {
        using (MemoryStream input = new MemoryStream(source))
        using (MemoryStream output = new MemoryStream())
        using (DeflateStream decompressedDstream = new DeflateStream(input, CompressionMode.Decompress))
        {
            decompressedDstream.CopyTo(output);

            byte[] destination = output.ToArray();

            Debug.Log("Decompress Size : " + output.Length);

            return destination;
        }
    }

その他の修正箇所

UnityRemoteVideo.cs

public void OnPreRender()
        {
            ~省略~

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureYMsgId, YByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureYMsgId, ByteConverter.ConvertByteCompress(YByteArrayForFrame(1 - currentFrameIndex)));

            //connectToEditor.SendToEditor (ConnectionMessageIds.screenCaptureUVMsgId, UVByteArrayForFrame(1-currentFrameIndex));
            connectToEditor.SendToEditor(ConnectionMessageIds.screenCaptureUVMsgId, ByteConverter.ConvertByteCompress(UVByteArrayForFrame(1 - currentFrameIndex)));
            
        }

ARKitRemoteConnection.cs

void ReceiveRemoteScreenYTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;

            //remoteScreenYTex.LoadRawTextureData(mea.data);
            remoteScreenYTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenYTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetYTexure(remoteScreenYTex);
            }

        }

        void ReceiveRemoteScreenUVTex(MessageEventArgs mea)
        {
            if (!bTexturesInitialized)
                return;
 
            //remoteScreenUVTex.LoadRawTextureData(mea.data);
            remoteScreenUVTex.LoadRawTextureData(ByteConverter.ConvertByteDecompress(mea.data));
            remoteScreenUVTex.Apply ();
            UnityARVideo arVideo = Camera.main.GetComponent<UnityARVideo>();
            if (arVideo) {
                arVideo.SetUVTexure(remoteScreenUVTex);
            }

        }

注意!

DeflateStreamクラスのCopyToメソッドを使うに当たってUnityで使用している.NET Framework3.5では使うことができなかったので .NET Framework4.6を使用しています。
Unity2017から利用できるのですが、Experimental(実験的)と記載されています。

ただ、このままビルドして端末にインストールするとエラーが出て動かないので
UnityARKitRemoteシーンのビルドは.NET Framework3.5、ビルド後に .NET Framework4.6に変更してEditorを実行するようにしてください。

これをする理由はCopyToメソッドを使いたいだけなのでデータの展開の際に同等の処理を実行できればなんでも良いので、面倒でしたら書き換えてしまって.NET Framework3.5で動くようにしてしまったほうが楽かもしれません。

結果

youtu.be

f:id:gounsx:20180404144521p:plain

圧縮後のデータサイズが赤文字です。

1フレームに送るデータ量を約1/3程度まで小さくすることができました。

表からも分かるようにiPhoneXではiPhone7に比べて2倍近いデータを送っていました。

これは推測ですが、Unityの中の人はiPhoneXでUnity ARKit Remoteのデバッグを行っていないのかもしれません・・・

紹介動画ではiPadで動作確認をしていました。

www.youtube.com

修正に入る前に念の為

  • Wifiをオフにして確実に優先で接続
  • サードパーティのケーブルではなく純正のケーブルを使用
  • 2014年のMacBook Proのスペックを疑い、2015年モデルでも確認

を行ったのですがどれも同じ結果でした・・・

何はともあれ、データを圧縮して送ることで無事動きました^^

これで実機にビルドしなくても、Editorに接続した実機上で動作確認することができるようになりました。

開発のイテレーションを速く回すためにはこうした環境作りも大切ですね。