リアルタイム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モデルに反映することで、そのキャラクターがよりその場に存在する実在感が生まれました。