見出し画像

Quest2単機での色味が違う問題についての考察・実験・調査まとめ

現在VRChatにおいてワールド制作をしていると、Quest2単体でワールドを確認した時の色味がPCVRやデスクトップとあまりにも違いすぎることがあります。

それについて私なりに解決すべく、良く分からないなりに色々調べてみたのですが、どうにも手詰まりな感じが出てきました。なので、一旦調べられた内容を記事としてまとめてみた次第です。
(まるで力尽きた探検家が残したメモみたいな感じですが、まさにそれです(笑))

具体的な症状と考察

下記は私の制作中のワールドを撮影したツイートになります。
左がQuest2単体で撮影したもの、右がUnityエディタ上で撮影したものです。
明らかに赤の色味が強すぎることが分かります。

ただ、撮影する時に気が付いたのですが、VRChatのカメラで撮影した画像ではUnityエディタ同様の正常な色味に見える一方、Quest2のスクリーンショットでは(Quest2単体で見る映像と一致する)異なった見え方をします。

つまりこのことから、「アプリ内映像としては正しいものが描画されているが、Quest2でレンズに表示される時に何らかの影響を受けている」ことが推測できます。

そして、今回問題となっているのは、
「Unity上で編集しても、Quest2単体だと色が違うので困る」
ということが主な問題です。

ワールド作ってる側として、『Quest2を被って実際に中に入らないと色味が分からない』というのはかなり苦行です。

したがって、私としては
「色が変わること前提で、Unity上をQuest2単体の状態に合わせる」
という方向性で行くことにしました。

それにあたり、まずはQuest2をUnity上でデバックする方法はないかと探すことにしました。

正攻法の発見、そして断念

幸いそれ自体はすぐに見つかりました。

Metaが公式で出しているOculusの開発環境とQuest Linkを使用することで、Unityのプレイモードに入るだけでデバッグが出来ることが分かりました。

またカメラコンポーネントの設定で、Quest2の色空間を設定することが可能になっているため、そのものの色味を出せるのではないかと期待しました。

またQuest上に表示される画面をOculus developer hubを使うことで、PC側にキャストできることも分かりました。

しかしながら、この正攻法とも言うべき方法は使えませんでした。

なぜなら、Unity Asset Storeに提供されているOculus IntegrationのバージョンがUnity2020以降となっており、VRChatのUnityバージョンは2019だからです。

試しにインポートしてみたところC#の言語バージョンが8.0の記法となっており、大量のエラーが発生しました。
(軽く調べたところ古いUnityバージョンでも、最新のVisualStudioの言語バージョンを使う方法はありそうでしたが、ひとまず後回しにしました)

Oculus Developer hub経由で、VRChatのワールドを常に中継し続ける方法は可能そうでしたが、更新のたびにワールドをリロードするのはちょっと残念な感じですし、色の調整には不便です。

Oculus Integrationの古いバージョンはUnityアセットストアではなくMetaの公式サイトを覗いたところ3.9までダウンロードが許されていますが、これがどのUnityバージョンに対応しているかを調べることはできませんでした。
(おそらくアセットストアが2020なのでそれに合わせていそうだなと感じました)

ちなみにUnity2019の時にOculus Integrationを使っている記事を見るとver1.4の辺りでしたので、Quest2側のファームウェアアップデートなどを考えると上手くいかなそうな気配を感じています。
(ちなみにアドレスを直打ちすることで無理矢理1.40を取得できたので、後で試してみようと思っていますが、これも後回しにすることにしました)

したがって正攻法は諦め、ちょっとした実験をして、各色ごとの補正値を割り出したうえで色の補正をかけてみることにしました。

実験によるデータ収集と補正

新規プロジェクトにAndroid環境を構築し、以下のようなグラデーションをチェックするプロジェクトを用意してみました。各色、値で言うと32ずつ遷移しています。各ブロックに使用したシェーダーはUnlit/Colorで、光源などの計算を受けないシンプルなシェーダーを利用しました。

これをUnity上と、Quest2単機で撮影した画像を使って、画素値の変化を確認しました。

画素値の調査方法は、本当は中心部の平均とか取るべきなのですが、面倒なので中心部分をペイントソフトのスポイトで探って、多く分布している値を採用しています。

以下はQuest2で撮影した画像になります。ぱっと見でも明らかに左下の赤ブロック二つが同じ色にしか見えません。

あまりにやる気のない図で申し訳ないですが、以下にグラフ化してみました。理想は斜め45度に一直線になっていることです(いい感じのグラフの作り方忘れてしまった……)

面白いことに白~黒のグラデーションは非常に綺麗に表現されています。一方で赤の画素値がかなりブーストされていることや、緑と青についても軽くブーストされていることも分かります。白~黒を厳密に合わせるために、他の色が色調整の影響を受けていそうだなという感想を抱きます。

このグラフだと上限が255になっているため分かりづらいですが、赤・緑・青のいずれも線形にブーストされていることが読み取れます。したがって簡単に補正をかけられることが分かりました。

そこで各種補正値を割り出し、各画素値に対して掛け合わせた上で再度撮影を行いました。その際使用した補正値はRGBの順で(0.802, 0.95, 0.95)です。
以下は補正後の写真とグラフになります。


多少ずれはありますが、補正をかけたことによりほとんど正確な画素値が得られていることが分かります。とはいえ、残念ながらこの補正値を私のプロジェクトに適応しても同じ見た目にはなりませんでした。これは予想されたことでした。

というのも、最初に赤い色になることに気が付いた時点で、大雑把にではありますが画素値の比較を行っていました。その結果ですが、明らかに画素値のブースト量が異なることが判明していたからです。

こちらに関しては線形のブースト量なのかは計算していませんが、いずれにせよプロジェクトごとに補正値が異なる可能性や、あまり考えたくはありませんが光源計算が行われる場合(非Unlitの場合)に補正値が異なる可能性などが考えられます。

以上より、全てのUnityプロジェクトに対して画一的な補正をかければ良いという簡単な話ではないことが分かりました。

そのためQuest2で表示されるまで、どのようなことが行われるのか……要は仕組みをキチンと知って対応する方向性を模索することにしました。

そして、「Unity Color Correction」などのキーワードで検索をかけて、情報を探っていたところ以下のリファレンスに辿り着きました。
Metaのドキュメントの一つです(なぜかMetaのサイトってプレビューできないみたいなので、リンクだけになります)

https://developer.oculus.com/resources/color-management-guide/

VRでの色空間について書かれた素晴らしいリファレンスなのですが、何故か検索に引っかからなかったので苦労しました(どこかのスレッドにリンクを載せていた人が居たので、そこから見つけることが出来ました)

以降の文章はこのリファレンスの内容を多分に含んだものになっていますが、私自身が間違って解釈している可能性もありますので、正確な内容を見たい人はリファレンスを確認することを推奨します。

さて、このリファレンスを読み解くと上記の現象に関係のありそうなことがあったので、そこを掘り下げることにしました。

・Quest2の色空間はRec.709、ただし白色点がD75
色空間についての知識はさっぱりだったのですが、この度色々調べたことで何となく理解しました。素人理解なので、以下で軽い説明をしますが誤った解釈が含まれている可能性はご了承ください。

色空間というのは、表現できる色の範囲を座標で表したもののようです。色空間は複数の種類があるため、使用する色空間によっては画面の色味が変わることがあるようです。

他の色の補正をする際には基準となる白を定める必要がありますが、「白」と一概に言っても色々な「白」があります。そこで基準となる白を決めて、それを白色点と呼んでいるようです。白色点を表す際には色温度(単位K,ケルビン)が使われているようです。

一般的にはD65(6500K)が基準として用いられており、Rec.709という色空間においてもD65が基準となっています。ですが、Quest2を初めとする各種HMDではD75を基準にしているようです。
一方でUnityはsRGB(ガンマ補正を除いてRec.709と同一)のようですので、白色点はD65だと思われます。

ちょっとここの理解が弱いので、自信がないのですが、
D75はD65よりも冷たい青系の色になりますので、これを基準の白にした場合、もともと暖色系の照明を受けている部分(つまり赤系の色味の部分)はより赤くなるのではないかと思います。

つまり基準となる白色点の違いによって、赤くなっているのかもしれないと私は考えました。とはいえ、たった1000Kの差なので、影響としてはほんの少しだと思ってはいましたが、手を動かして確認してみることにしました。

Quest2の色空間への変換をするシェーダーを実装し、Graphic.blitを利用して、ポストプロセス的に色空間を変換してみようと思ったのです。

ここで一つ考えなければいけないのは、色空間はVRアプリ側で指定できるということです。実は色空間はHMDによって違うらしく、例えば初代QuestはRec.2020という、Rec.709と比較すると遥かに広い色域を持つ色空間を採用していたようです。
(確かにVRアプリ側で、このHMDはこの色空間!って指定できるようになってないと、困りますよね)

色空間への知識がほぼゼロだったので、違った色空間を使ってたらガラッと色が違ったりするのかなと思ってたんですが、実装する過程でそんなことはないんだなと実感しました。

例えばRec.2020を基準にコンテンツを作ってて、Rec.2020→Rec.709のような広い帯域から狭い帯域に押し込める場合はガラッと変わる場合はありますが、UnityとHMDは対して違いのない色空間なので対して違いはなさそうでした。

とまぁ、結果を少し述べてしまいましたが、ひとまずQuest2向けの色空間変換を実装してみました。

使用した各CIE座標とかは以下の通りです
・Quest2環境(Rec.709 D75想定)
Wxyz = (0.298, 0.318, 0.384)
Wr = (0.64, 0.33 , 0.003)
Wg = (0.30、0.60、0.10)
Wb = ( 0.15、0.06、0.79)
Unity環境(sRGB D65想定)
Wxyz = (0.31271、0.32902、0.35827)
Wr = (0.64、0.33、0.03)
Wg = (0.30、0.60、0.10)
Wb = ( 0.15、0.06、0.79)

何かミスがある可能性があるので以下にシェーダーとスクリプトのコードを晒しておきます。変換用の逆行列などは算出用のpythonコードを掲載しているサイトがあったので、そちらを使って導出しました。

Shader "PostEffect/QuestColor"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;

            fixed4 frag (v2f i) : SV_Target
            {
                float4 col = tex2D(_MainTex, i.uv);
                // just invert the colors

                //Quest2
                float3 temp = mul( float3x3(3.61048,-1.712664,-0.5554585,-0.9522346,1.843047,0.04082581,0.04947107,-0.1813939,0.9399502), mul( float3x3(0.4124,0.3576,0.1805,0.2126,0.7152,0.0722,0.0193,0.1192,0.9505), col.rgb ) );
                
                //Quest1
                //float3 temp = mul( float3x3(2.302998,-0.7044615,-0.3632426,-0.9003402,1.759528,0.06584688,0.03794319,-0.07617083,0.8506576), mul( float3x3(0.4124,0.3576,0.1805,0.2126,0.7152,0.0722,0.0193,0.1192,0.9505), col.rgb ) );

                //Rift CV1
                //float3 temp = mul( float3x3(2.316,-0.7485161,-0.3493226,-0.9035893,1.801768,0.03725893,0.03475524,-0.06930236,0.8585445), mul( float3x3(0.4124,0.3576,0.1805,0.2126,0.7152,0.0722,0.0193,0.1192,0.9505), col.rgb ) );
                
                col= float4(temp,col.a);
                return col;
            }
            ENDCG
        }
    }
}

Blit用スクリプト

using UnityEngine;

[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class PostEff : MonoBehaviour
{
    [SerializeField] private Shader _distanceFogShader;

    [SerializeField] private Material _material;

    private void OnRenderImage(RenderTexture src, RenderTexture dest)
    {
        if (_material == null)
        {
            _material = new Material(_distanceFogShader);
        }

        Graphics.Blit(src, dest, _material);
    }
}

実際に使ってみた比較は以下の通りですが、まぁ予想通り若干オレンジになったな~くらいの感想でした。他にもRift CV1や初代Questのバージョンも実装してみたのですが、ほぼ変わらない感じでした。

エフェクト非適応
エフェクト適応

おそらくここから黒~白のグラデーションをきちっと合わせるために、各色に対して色補正をかけて~って感じの流れだと思うんですが、そこら辺の処理については書かれてなかったのでここで手詰まりでした。

ですが、これも要因の一つである可能性は高そうというのは分かりました。

暫定的な対処方法

本来は原因を突き詰めたかったところではありますが、これ以上分からなかったのでひとまずはワークアラウンドで何とかすることにしました。

途中で行った実験が正しいとすると、画素の誤差は線形になっているようです。

したがって、先ほど作ったシェーダーを改造し、赤・青・緑をそれぞれ調整できる簡単なシェーダーを作成しました。Graphic.blitを利用して、ポストプロセス的に画面の色を変更します。
また、色が白に近いほど、そのままの色を返す処理を入れています(白~黒のグラデーションはそのまま維持したかったため)

Shader "PostEffect/QuestBoost"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _RedBoost ("RedBoost", Range (0, 5)) = 1
        _BlueBoost ("BlueBoost", Range (0, 5)) = 1
        _GreenBoost ("GreenBoost", Range (0, 5)) = 1
        _Threshold ("Color Difference Threshold", Range(0, 1)) = 0.05
    }
    SubShader
    {
        // No culling or depth
        Cull Off ZWrite Off ZTest Always

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            sampler2D _MainTex;
            float _RedBoost;
            float _BlueBoost;
            float _GreenBoost;
            float _Threshold;

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                // just invert the colors

                float maxColor = max(max(col.r, col.g), col.b);
                float minColor = min(min(col.r, col.g), col.b);
                float diff = maxColor - minColor;

                if(diff <= _Threshold)
                {
                    //差が小さい場合はdiffの値が小さいほど元の色(つまり白)の比率を高くする
                    float3 temp = col.rgb;

                    float param = diff/_Threshold;

                    temp.r *= _RedBoost;
                    temp.b *= _BlueBoost;
                    temp.g *= _GreenBoost;

                    temp = lerp(col , temp , param);
                    
                    col.rgb = temp;


                }else
                {
                    col.r *= _RedBoost;
                    col.b *= _BlueBoost;
                    col.g *= _GreenBoost;
                }

                return col;
            }
            ENDCG
        }
    }
}

アップロード時は、Graphic.blitを普通のC#スクリプトで作っているので、スクリプトはVRCSDKによって除去され、勝手にエフェクトが外れた状態でアップロードされるという形です。

適当にやるならQuest単機で写真撮って、その写真と一致するように値を調節して、終了です。ただし彩度が高すぎると255で値が止まってしまうので、色薄めの状態にする必要があります。

写真はOculusボタン+トリガーで撮影します。撮った写真はQuest2とUSBで接続し、Quest2の画面で操作を許可することで、出てきたデバイスのOculusフォルダのScreenshotsフォルダ内にあります。

適当に写真に合わせる感じでやってみましたが……結構難しいです。左からUnity上未補正、Quest2単機、Unity上補正後です。夕暮れの海の色を合わせるように調整してみましたが、おおむね似ているけど若干違う感じになってしまいます。

しっかりやるならカラーチェッカーが必要そうな気がしますが、それでもないよりはマシくらいの役割は果たしてくれそうです。

あとでパッケージ化したらこの下あたりに投げて置く予定ではありますが、説明文とか諸々書かないと難しいと思うのと、しばらく忙しいので気長にお待ちください。
(代わりに誰かもっといいやつ出してくれても良いですよ!)

おわりに

というわけで、原因解決はできなかったので、色々試してみた記録を残しておきます。

なんかイイ感じのやり方を知ってる人or 見つけた人 or そもそもなぜ起こるのか知っている人は教えてくれたらとても嬉しいです。

Quest対応の手間が増えると、そもそもQuest対応してくれるワールド製作者さんが減るという、結構切実な問題だと思うので何かしらいい方法が見つかるといいな~と思っています。




この記事が気に入ったらサポートをしてみませんか?