UnityのPlane上に注視点ヒートマップを作る

#目的
VR空間での視線情報の可視化。
個人的覚え書き。

ヒートマップのリポジトリ：https://github.com/sakamo1290/FixationMap
#方針
Shaderで注視点を中心に赤→緑→青と色分けする。
時間経過とともに青→緑→赤となるようにする。
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/f87291f2-5118-0bc7-9328-4210c36ec13f.gif)

#色の変化
###注視点を中心に赤→緑→青とする。
HSV変換を用いる。
HSVは色相(Hue),彩度(Saturation),明度(Value)の3成分からなる色空間のこと。
色相は具体的な色を定義する要素。色が環状に並んでいるため0°~360°で表される。(今回の実装では0~1に正規化している)
![hsv](https://upload.wikimedia.org/wikipedia/commons/f/f1/HSV_cone.jpg)
[ここ](https://www.peko-step.com/html/hsv.html)で示されているように色相は0°~240°にかけて赤→緑→青と変化するためこれを利用する。
正規化されているため色相が0なら赤,0.66なら青になる。
HSV変換の実装は [このページ](https://techblog.kayac.com/unity_advent_calendar_2018_15 )を参考にした。


```glsl:GazeMap.Shader
			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
				float4 worldPos : TEXCOORD1;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			float4x4 _TransformMatrix;
			float4 _CubePos;

			v2f vert(appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				UNITY_TRANSFER_FOG(o,o.vertex);
				//uv座標をワールド座標に変換
				o.worldPos = mul(_TransformMatrix, float4(-(v.uv.x - 0.5) * 10, 0, -(v.uv.y - 0.5) * 10, 1));
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				UNITY_APPLY_FOG(i.fogCoord, col);
				//ヒートマップの効果範囲(仮置き)
				float maxDia = 0.5;
				//マッピングするための重み
				//1.5秒で赤くなるように135で割る(fps:90)
				float probability = (clamp(maxDia - distance(i.worldPos, _CubePos), 0, maxDia)) / 135;
				//hsvに変換
				float3 hsv = rgb2hsv(float3(col.r, col.g, col.b));
				//1frame前のヒートマップに重畳
				float h = hsv.y == 0 ? 0.66 - probability : hsv.x - probability < 0 ? 0 : hsv.x - probability;
				//効果範囲内なら色を変える
				return maxDia> distance(i.worldPos, _CubePos) ? float4(hsv2rgb(float3(h, 1, 1)), 1) : col;
			}
```
###時間経過とともに青→緑→赤
bufferで1つ前のフレームの計算結果を使う

```c#:MainGazeMap.cs
    public Material iniMat;
    public Material paintMat;
    public Transform gazePoint;

    Material mainMaterial;
    RenderTexture mainTex;
    void Start()
    {
        mainTex = new RenderTexture(770, 1000, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default);
        Graphics.Blit(mainTex, mainTex, iniMat);

        mainMaterial = GetComponent<Renderer>().material;
        mainMaterial.SetTexture("_MainTex", mainTex);
    }
    void FixedUpdate()
    {
        //bufferを作成
        RenderTexture buf = RenderTexture.GetTemporary(770, 1000); ;
        //paintMatを更新
        MatUpdate();
        //mainTexとpaintMatを使ってbufferに描画
        Graphics.Blit(mainTex, buf, paintMat);
        //bufferの内容をmainTexに上書き
        Graphics.Blit(buf, mainTex);

        RenderTexture.ReleaseTemporary(buf);
    }
    void MatUpdate()
    {
        paintMat.SetVector("_CubePos", new Vector4(gazePoint.position.x, gazePoint.position.y, gazePoint.position.z, 1));
        paintMat.SetTexture("_MainTex", mainTex);
        paintMat.SetMatrix("_TransformMatrix", transform.localToWorldMatrix);
    }
```

#動かす
3DオブジェクトのPlaneを配置して名前をGazeMapにする。
Cubeを作って名前をGazePointとしてGazeMap上に置く。
MainGazeMap.csをアタッチして初期化マテリアルとペイントマテリアルを配置する。
![unity image.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/83b32408-92d3-165b-3b63-910faf2fe338.png)
初期化マテリアルはRendering ModeをFadeにする。
![defaltMat.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/f485f18a-7aa5-c23e-7973-49f5bdbd43ea.png)
ペイントマテリアルは作成したGazeMap.Shaderをあてる。
![paintMat.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/0248905c-0123-a8e4-18f3-82929c4d2cf7.png)
これで動かすと下の画像のようになる
![demo.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/23fdbc62-8092-31d7-abfe-e743673f78d4.gif)

#視野を考慮してマッピング範囲を決める
人間の視野は中心2°が鮮明に、5°までをぼんやりと認識している。
そのため中心部分の重みを大きくしてより効果的な可視化を行いたい。
![GazeSpan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/ad7397d5-208a-9658-ef66-6547a7765d0c.png)
そのために正規分布を変形して重みづけをする。
平均μ,標準偏差σの正規分布の式は

```math
f(x)=\frac{1}{\sqrt{2πσ^2}}exp(-\frac{(x-μ)^2}{2σ^2})
```
今回は注視点を中心にするため平均は0、必要なのは確率密度ではなく重みづけがしたいだけだから係数は1とする。

すると重みづけの式は

```math
f(x)=exp(-\frac{x^2}{2σ^2})
```
この式のxは描画点の角度に相当するためσを求めればよい。
上の図から今回は角度が1°の時に重みが半分になるようにする。
そのために[半値全幅](https://ja.wikipedia.org/wiki/%E5%8D%8A%E5%80%A4%E5%B9%85)(full width at half maximum, FWHM)からσを求める。

```math
FWHM=2\sqrt{2ln2}σ\approx2.3548σ
```
FWHMは1°/2.5°より0.4となり、また正規分布が2.5°付近で0に収束するように5をかけると

```math
σ=\frac{0.4×5}{2.3548} = 0.84933
```
指数の肩の整数部分を計算すると

```math
\frac{1}{2σ^2}=\frac{1}{2(0.84933)^2}=0.693135
```
最終的な重み関数の式は

```math
f(x)=exp(-{0.6931×x^2})
```
これを用いてGazeMap.ShaderとMainGazeMap.csを修正する。
視線の開始位置の情報を加える。

```glsl:GazeMap.Shader
			fixed4 frag(v2f i) : SV_Target
			{
				fixed4 col = tex2D(_MainTex, i.uv);
				UNITY_APPLY_FOG(i.fogCoord, col);
				//対象ピクセルの角度を計算
				//180÷3.1415 = 57.297を使ってラジアンを度数法に変換
				float angle = atan2(distance(i.worldPos, _CubePos), distance(i.worldPos, _GazeOri)) * 57.297;
				//正規分布を使って中心に重みづけする。
				float probability = exp(-0.6931 * angle * angle)/135;
				float3 hsv = rgb2hsv(float3(col.r, col.g, col.b));
				float h = hsv.y == 0 ? 0.66 - probability : hsv.x - probability < 0 ? 0 : hsv.x - probability;
				//視野範囲内ならマッピングをする
				return angle < 2.5 ? float4(hsv2rgb(float3(h, 1, 1)), 1) : col;
			}
```

```c#:MainGazeMap.cs
    public Transform gazeOrigin;
    void MatUpdate()
    {
        paintMat.SetVector("_CubePos", new Vector4(gazePoint.position.x, gazePoint.position.y, gazePoint.position.z, 1));
        //追加
        paintMat.SetVector("_GazeOri", new Vector4(gazeOrigin.position.x, gazeOrigin.position.y, gazeOrigin.position.z, 1));
        paintMat.SetTexture("_MainTex", mainTex);
        paintMat.SetMatrix("_TransformMatrix", transform.localToWorldMatrix);
    }
```
修正して動かすと画像のようになる。
円周付近の色の変化が遅くなっていることがわかる。
![gaussian.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590438/204e5b69-f01b-1fea-3a87-fc721db68980.gif)


#参考
[【Unity】RGBをHSVに変換して明るさとかを変えるシェーダー](https://techblog.kayac.com/unity_advent_calendar_2018_15 )
[UnityでRenderTextureをファイルに保存](https://psychic-vr-lab.com/blog/unity/unity%E3%81%A7rendertexture%E3%82%92%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AB%E4%BF%9D%E5%AD%98/)
Blignaut, Pieter. (2010). Visual span and other parameters for the generation of heatmaps. Eye Tracking Research and Applications Symposium (ETRA). 125-128. 10.1145/1743666.1743697.