地味だけどARと現実を地続きに表現するために重要な "画面揺れ"の実装

## **STYLYアドベントカレンダー**

この記事は『[STYLYアドベントカレンダー](https://qiita.com/advent-calendar/2021/styly)』21日目の記事です。

## はじめに

皆さん、ARコンテンツ、作ってますか?
STYLYもそうですが、最近はARコンテンツが簡単に作れるようになってきました。

今回はそんなARコンテンツに **現実との地続き感** を簡単に出せる、
「画面揺れ」の実装について紹介していきます。

本記事ではSTYLYの実装をベースとしてますが、基本はシェーダーによるポストエフェクトなので、Unityを用いたAR開発ではそのまま流用が可能です。
ARFoundationや最近だとLightshipなどもありますが、そのまま導入が可能です。

## 「画面揺れ」の実践例

画面揺れというと、どういう演出ができるのか。

これは実際に見ていただいて、また体験していただくのが一番伝わると思いますので、
自分の作品でいくつか実践しているものを紹介させていただきます。
どちらの作品もSTYLYに公開していて、体験できるものなので、実際に体験してみてその効果をまずみていただけると幸いです。

### 「Augumented "Baberuga Gravidon"」
<iframe width="560" height="315" src="https://www.youtube.com/embed/XlscDxnPrjw" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

https://gallery.styly.cc/scene/6b8c0743-ba92-4c67-a595-b349db5f8848

・某アニメの重力呪文を再現したAR演出です。
　最初に光のエフェクトに合わせて短く数秒、その後の重力のようなエフェクトに合わせて長く画面揺れを入れています。

### 「See there / ここに見る」

<iframe width="560" height="315" src="https://www.youtube.com/embed/9P3RTn2M6ww" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

https://gallery.styly.cc/scene/3441a3dc-f097-4e2c-938c-5dc083b4a385

・STYLYさん主催のNEWVIEW AWARDS 2021で応募、ファイナリストに選出いただいた作品。
　23秒あたりから、地面が盛り上がり、沈んでいくシーンで画面揺れを使用しています。

見ていただけると分かるとおり、何も画面効果を入れない場合より、
グッと **「そこで何かが起こっている感」**が増します。
特に、実際にみている側の体験感は映像で見るよりグッと上がります。

今回はこの実装を誰でも取り入れられるように解説していきたいと思います。

## 普通のアプローチで、できない理由

※ この章は普段ネイティブでARコンテンツ開発している人にとってはイメージがつくかもしれないので、ご存知の方は「実装のアプローチ」の項目まで飛ばしていただいて構わないです

まず、実装に入る前に、アプローチ方法を検討します。
シンプルに考えると、「Cameraオブジェクトを揺らせばいいんじゃないの?」と思われるかもしれません。

ただし、__その方法では (STYLYやUnityでARFoundation準規のARコンテンツ開発の場合) できません。__

その理由は、「ARにおいては、現実のカメラ映像は背景として平面に描画される」 からです。
以下、VRコンテンツと比較して説明していきます。

VRコンテンツにおいては、コンテンツ内のCameraオブジェクト = 仮想上のカメラが見ているものが、実際にVR上の視点の映像となります。
そのため、背景含み、全てその「Cameraオブジェクトが見ている映像」になるので、そのCameraが揺れれば、視点全体も揺れます。

``※ イメージです。白いオブジェクトがメインのオブジェクト、青いオブジェクトが背景と考えてください。``
![shake.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/5b69ef47-fd5d-883a-9237-f89bba3f9a36.gif)
ただし、ARコンテンツにおいては、


* Cameraオブジェクトが見ている映像

が投影された後に、

* 現実のカメラの映像が平面的に背景に

投影されます。

イメージとして、

* 「Cameraオブジェクトが見ている映像」 と 「現実のカメラ映像」の二つのレイヤーがある
* 「現実のカメラ映像」はカメラに追従している (= カメラの位置の影響を受けない)

と想像してもらえればわかりやすいです。
``※正確な原理としては違いますが、ここはイメージしやすさのため、ここでは上記のように説明しています``

そのため、そのままカメラを揺らしても、以下のイメージのような感じに、
![shake2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/708fa2cb-97b0-118c-284b-1cef1128f3a8.gif)
__「Cameraオブジェクトが見ているARの物体」は揺れますが、「現実の映像」自体は揺れてくれません。__

以上が、Cameraオブジェクトを揺らしてもARでは画面揺れを実装できない理由になります。
では、どうやって「カメラ映像を含めた映像を揺らすのか」というアプローチです。

## 実装のアプローチ

映像のレンダリング前の「Cameraオブジェクト」を揺らすだけでは実現できないことは分かったので、
「ARの背景映像も含んだ最終出力結果の映像」を動かすアプローチをとります。

つまり、ポストエフェクトとしてシェーダーの実装を行います。
以下、今回実装するシェーダーのコード全文です。

```hlsl
Shader "Custom/Camera/ShakeAuto" { 
    Properties {
        _MainTex ("Source", 2D) = "white" {}
        _X ("X",Float) = 0.01 //X座標の揺らしたい値
        _Y ("Y",Float) = 0.01 //Y座標の揺らしたい値
        _ShakeSpeed ("ShakeSpeed",Float) = 100 //揺らす速度の参照値
    }

    SubShader {
        
        ZTest Always
        Cull Off
        ZWrite Off
        Fog { Mode Off }

        Pass{
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #include "UnityCG.cginc"

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

            v2f vert(appdata_img v) {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = MultiplyUV(UNITY_MATRIX_TEXTURE0, v.texcoord.xy);
                return o;
            }

            sampler2D _MainTex;
            float _X;
            float _Y;
            float _ShakeSpeed;

            fixed4 frag(v2f i) : SV_TARGET {

                float2 rand = fixed2(sin(_Time.y*_ShakeSpeed),cos(_Time.y*_ShakeSpeed));
                float2 uv = float2(i.uv.x + _X*rand.x,i.uv.y + _Y*rand.y);

                return tex2D(_MainTex, uv);
            }
            ENDCG
        }
    }
    FallBack Off
}
```

やっていることはとてもシンプルで、
__「ポストエフェクトのUV座標をランダムに動かし続ける」__ だけです。

実装でいうと、今回に関わるのはこの部分だけです。

```
fixed4 frag(v2f i) : SV_TARGET {
　　　float2 rand = fixed2(sin(_Time.y*_ShakeSpeed),cos(_Time.y*_ShakeSpeed));
　　　float2 uv = float2(i.uv.x + _X*rand.x,i.uv.y + _Y*rand.y);

　　　return tex2D(_MainTex, uv);
　}
```

rand変数は、擬似的なランダムな数値を生成するための関数です。
そこに、引数として_Timeで取得した値を、振動させたい速度 (_ShakeSpeed) で更新させ、
その値分、fragmentシェーダー部分でX座標とY座標をずらします。

シンプルではありますが、これで画面を揺らしたような見せ方を実現できます。
以下、上のシェーダーでの画面揺れだけを実装したサンプルのSTYLYシーンです。
このようなものを今回STYLYで作っていきます。

https://gallery.styly.cc/scene/68c14ff0-4117-4d49-b9f4-776836173979

このポストエフェクトを使った、STYLYを想定した実装方法について、流れを説明していきます。
基本は上記のシェーダーをPlayMakerのカスタムアクション「Set Image Effect」を使用して設定するだけです。
細かい点については、以下の記事に詳しく紹介されていますので、こちらもご参照ください。

https://styly.cc/ja/tips/set-image-effect-custom-action/

以下、流れの説明になります。

① 上記シェーダーを作成して、マテリアルを作成します。
![スクリーンショット 2021-12-12 15.52.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/c89947dd-4e10-0b44-9ee5-5599d7b5563a.png)

② 適当なゲームオブジェクトを作成し、そのオブジェクトにPlayMakerWindowで「Add FSM」を選択
![スクリーンショット 2021-12-12 15.54.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/85ae880e-1057-39b4-c30b-f8c3742a2076.png)

③ 「Action Browser」から「Set Image Effect」を検索し、Stateに追加。そのMaterialに①で作成したマテリアルをセットします。
![スクリーンショット 2021-12-12 15.56.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/4d046539-5cca-12f0-3145-8181db1e4f3c.png)

④ STYLYにアップロードして、配置して完了です。
　揺れ方が気に入らない場合は
　_*X 、_*Y、_ShakeSpeed の値を好みに調整してください。
![スクリーンショット 2021-12-12 16.00.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/de7a1f04-4559-6598-a0ae-adf16881587c.png)

こんな感じに画面揺れが実装できます。
![shake3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/4d4249b8-32e9-ecb4-9166-d091b4b041c3.gif)

## ON/OFFの切り替えについて

このオブジェクトのままアップロードして配置してしまうと、画面が揺れっぱなしになってしまいます。やはり、ON/OFFは行いたいところ。

ON/OFFの切り替えについては、Playmaker等でカスタムアクションを制御したりするなどでもできますが、一番簡単に制御する場合はSet Image Effectをセットしたオブジェクト自体をアクティブ/非アクティブの切り替えが一番簡単です。

AnimatorでもTimelineでも制御が簡単にできるので、自分はよくこの方法で演出に合わせて発火・停止をしています。

例えば、STYLYにアップロードする際は、演出全体として大きなゲームオブジェクトの配下に各種エフェクトをセットし、先ほど設定したポストエフェクト のオブジェクトも配置して、アップロードするのが良いでしょう。
![スクリーンショット 2021-12-12 16.00.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252231/ee5d05b0-fadf-0c39-6f85-4607e44729d1.png)

## STYLY以外の場合

STYLY以外の場合で、基本的にポストエフェクトのシェーダーは流用ができるので、ベースは同じです。そのままUnityで開発しているARアプリなどでご使用ください。

もし、PlayMakerを使わない場合などは、CustomImageEffectとして実装する形になります。
以下、少し古いですがシンプルなポストエフェクト実装の参考記事になりますので、そちらをご参照ください。

https://qiita.com/Hirai0827/items/4946ee4b8b52d6f1da27

## 最後に

内容的にはすごく地味ではありますが、割と効果的な画面揺れについての記事でした。

個人的に、ARコンテンツにおいてはARで起こっていることを  **「AR空間」ではなく「現実と地続きに広がっている空間」と捉える** と、こういう「ARで起こっていることがこちらにもフィードバックがある」ような演出は必須だと思っています。

個人的に最近はこのように「より現実的に感じるAR演出」や「現実の延長上に感じる表現」について色々試行錯誤しています。普段作品や試した結果を[Twitterの方](https://twitter.com/kiyossy0715)で公開していますので、よろしければフォローしていただけると幸いです。

最近はこんな感じのものを作ってます↓

https://twitter.com/kiyossy0715/status/1472059152390561794