VRChatのアバターでBadAppleを再生できるアセットを作成した話

# はじめに
ちゃみんと申します。
今回は趣味でアバターなどでも使えるUnity3DのBadAppleアセットを作成したので紹介しようと思います。
なお今回のプログラムはすべて自作したわけではなく、こちらの方のプログラムを改造して使用しております。(使用、改変許可確認済み、MITライセンス)

https://booth.pm/ja/items/1028680

# Download

[作成したアセットはここからダウンロードできます。](https://drive.google.com/file/d/1oMPtd384XCTXNciVCn5aiknQdMJ4L6tJ/view?usp=share_link)

# 目次

<!-- タイトルとアンカー名を編集 -->
1. 作ろうとしたきっかけ
1. 苦労した話
1. 作成したシェーダープログラムとスクリーンショット
1. 最後に

# 作ろうとしたきっかけ

昨年VRChatのフレンドに連れられ、ShaderFesというイベントに参加し、様々な創作物を見たり体感したりした際、

### 「僕もこんな感じのものが作りたい」

と感じ、制作するに至りました。

~~最初のうちは正直自分でも作れると思っていなかった。~~

# 苦労した話

VRChatのアバターを少しでもいじられた方ならご存じだと思いますが

### 2022年12月01日現在、基本アバターに仕込めるユーザー作成スクリプトはShaderlabのみ

です。
当然のことながらC#派生のUdonSharp(通称U#)は使えないです。
そのような事から結構理不尽な仕様がたくさんあったので、紹介しようと思ったのですが、きちんと書いた方がいいような気がしてきたのでシェーダーとは何なのか説明します。

### シェーダー（英: shader）とは、3次元コンピュータグラフィックスにおいて、シェーディング（陰影処理）を行うコンピュータプログラムのこと。「shade」とは「次第に変化させる」「陰影・グラデーションを付ける」という意味で、「shader」は頂点色やピクセル色などを次々に変化させるもの（より具体的に、狭義の意味で言えば関数）を意味する。 

https://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%A7%E3%83%BC%E3%83%80%E3%83%BC

まあつまるところ影をつける時のプログラムです。
普通であればモデルデータに影をつけるだけのプログラムなのですが、、、


よく上の引用文を見てください。

### 頂点色やピクセル色などを次々に変化させるもの

と書いてあるので

### ひょっとしたら頂点のUV座標をずらしてやればMP4のような動画再生プロトコルができるんじゃね？

と思い、まずモデルデータのUV座標をずらすプログラムをググりました。
幸い、このことに関しては先駆者の方が多数おり、そのうちの一人であるホタテの貝殻さんのプログラムが偶然Boothで無料配布されており、こちらのプログラムを改造して使うことにしました。

このプログラムは横に連番出力したプログラムを連結して動かすという昔のフィルム映画のような仕組みで動いており、基本的にVRChatが起動したタイミングでシェーダーのプログラムが実行されます。

### ん？

盲点でした。

そうなんです。よくあるパターンなのですがUnityEditor上だときちんと動くプログラムがVRChatではうまく動かないのです。
理由は単純です。VRChatは基本的にSteam上のプレイボタンを押したタイミングからプログラムの実行が始まります。
要はメアドとパスワードを入れるログイン画面やワールドに入る際のロード画面の段階でシェーダーファイルが実行されてしまうのです。
これをされてしまうと何が問題か、そうです。

### オーディオソースで指定した音楽とシェーダーの方で指定した動画再生プロトコルが同期しない

正直このことでかなり苦労しました。
今思うと、一週間くらいずっと考えていました
色々な人に助言をもらったりもしました。

考えに考えた末の結果

### シェーダーだけで解決しようとするな。アニメーションがあるじゃないか！

無事解決しました。
今までのプログラムは、次のフレームに移管する際、
以下のプログラムで動かしていましたが、アニメーションで動かすために不要になりました。

```
int maime = ceil(_Time/_timed)%_maisuu;
```

ザックリ言うとこの部分はおそらく、裏でぐるぐる動かすための文です。
不要なので削除します。
しかし、このままだと静止画になってしまうので上に以下の関数を追加

```
_maime ("毎目", int) = 0
```

これは主に動画の現コマ数(YouTubeでいうところのシークバーにあたるもの)を管理するために使用しています。
そして、不要になって削除したプログラムの中にtimed変数というものが使われていたので

```
uniform float _timed;
```

これを

```
uniform int _maime;
```

これに置き換えました
型宣言大事。
完成しました。


# 作成したシェーダープログラムとスクリーンショット

変更前

```
Shader "HOTATE/IMGtoMOVIE"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_maisuu ("枚数", float) = 1
		_timed ("間隔", float) = 0.1
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			uniform float _maisuu;
			uniform float _timed;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				int maime = ceil(_Time/_timed)%_maisuu;
				o.uv.x /= _maisuu;
				o.uv.x += maime*(1/_maisuu);
				UNITY_TRANSFER_FOG(o,o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
	}
}
```

変更後

```
Shader "HOTATE_And_Chamin/IMGtoMOVIECustom"
{
	Properties
	{
		_MainTex ("Texture", 2D) = "white" {}
		_maisuu ("枚数", float) = 1
		_maime ("毎目", int) = 0
	}
	SubShader
	{
		Tags { "RenderType"="Opaque" }
		LOD 100

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			// make fog work
			#pragma multi_compile_fog
			
			#include "UnityCG.cginc"

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

			struct v2f
			{
				float2 uv : TEXCOORD0;
				UNITY_FOG_COORDS(1)
				float4 vertex : SV_POSITION;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;
			uniform float _maisuu;
			uniform int _maime;
			
			v2f vert (appdata v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.uv = TRANSFORM_TEX(v.uv, _MainTex);
				o.uv.x /= _maisuu;
				o.uv.x += _maime*(1 / _maisuu);
				UNITY_TRANSFER_FOG(o, o.vertex);
				return o;
			}
			
			fixed4 frag (v2f i) : SV_Target
			{
				// sample the texture
				fixed4 col = tex2D(_MainTex, i.uv);
				// apply fog
				UNITY_APPLY_FOG(i.fogCoord, col);
				return col;
			}
			ENDCG
		}
	}
}
```

https://www.youtube.com/watch?v=R_IvDSygMok

スクショ
![BA - BadApple - Android - Unity 2021.3.5f1 Personal _DX11_ 2022_12_01 22_16_22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997807/e954eef1-13ee-afec-93bd-1299d1a7a87a.png)
このアドレス打ちは全部手作業でやりました。
~~地獄~~

マテリアルとテクスチャ
![BA - BadApple - Android - Unity 2021.3.5f1 Personal _DX11_ 2022_12_01 22_18_43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997807/3ad2980f-4c76-31d3-2068-c687d7245a70.png)
![BA - BadApple - Android - Unity 2021.3.5f1 Personal _DX11_ 2022_12_01 22_18_58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2997807/1eebb34a-51f0-5b1f-2e70-877a2ba70fe1.png)
3分くらいの動画を流すために用意したマテリアルとテクスチャ群
~~圧縮技術が未熟なもんでこれが限界~~


# 最後に
自分では無理だと思うこともきちんとチャレンジしていればいつかはできるようになるかもしれません。
最初のうちはできそうにもなかったのですが、いろいろな方に情報を頂いたりしてここまで成長することができました。
継続は力なり。
ぜひとも皆さんもいろいろなことにチャレンジしてみてください。

Twitter、GitHubなどで情報発信中です。
フォローの方、していただけるとめちゃくちゃ喜ぶのでよろしくお願いします。
- [Twitter](https://twitter.com/Chamin_VRC)
- [GitHub](https://github.com/Chamin-programmer)