Three.jsでBoneアニメーションをShaderMaterialで動かす

##はじめに

[Threejs](https://threejs.org/)はwebglにおける大体のめんどいことをやってくれるので非常にありがたい。しかしボリュームがボリュームだけに闇も多いので、ハマる時はハマる。もっと備忘録が増えればいいと思う。

[demo](https://codepen.io/ykido/pen/QdZVMx)

![image](https://s3-ap-northeast-1.amazonaws.com/ky-site/model/captured.png)



##今回のハマりどころ

Blenderでエキスポートしたアニメーション付のモデルをperlin noise的なエフェクトをかけてうにゅうにゅさせたくなったので、ShaderMaterialを使用してShaderを書いた所、jsonに記載されているスケルタルアニメーションが動かなくなった。

```js


this.myMesh = new THREE.SkinnedMesh(geometry, new THREE.ShaderMaterial({
              uniforms: this.uniforms,
              vertexShader: document.getElementById('vertex-shader').textContent,　
              fragmentShader: document.getElementById('fragment-shader').textContent,
              shading: THREE.FlatShading,       
              side: THREE.DoubleSide,
              skinning: true       
              },false));

.
.
.

this.mixer = new THREE.AnimationMixer(this.myMesh); // ミキサー生成
this.animClip = this.mixer.clipAction(geometry.animations[0], this.myMesh) //本来ならばここでアニメーションが登録されて、いつでも呼び出せる
```
上記のコードのままモデルをsceneに足しても、初期フレームの状態で固まってうにゅうにゅするだけ。
まぁそれはそれでよいのですが、やはりアニメーションも一緒に動かしたい。

需要があるかわからないけどゲーム開発なんかをしている人がいたら、多分同じところでハマると思うので備忘録をのこします。

ちなみにBlenderでのモデル出力方法は[ここらへん](http://yomotsu.net/blog/2015/10/31/three-r73-anim.html)を参考にすればだいたい分かる。若干古いが特に問題はなかった（POSE,REST等の設定がめんどい。そこらへんはよく読むと良い）。



今回の件とは関係ないが、[C4Dのエキスポーター](https://github.com/BlackDice/Cinema4D-Threejs-Exporter)を開発している人がいた。えらい。


##原因
関係ありそうなissueが本家レポジトリにも上がっていたが、「そもそもShaderMaterialを使うということは、モデルの描画に関係するすべての処理をオレが書くんだっ！ってことだから、そりゃ動かんわ。」とツッコまれていた。凡ミス。


##対処法
モデルのboneMatの座標から引っ張り出した値をskinVertexに毎フレーム反映させれば意図通りに動くんだろうな〜など...
が、さすがThree.js。どうやら[ShaderChunk](https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk.js)というオブジェクトに共通で使用されるGLSLのコードをまとめてくれていた。とても親切！
ここから適当にskinningに関係ありそうなコードを引っ張ってくる。

```js
vertexShader: [
   "uniform float offset;",
   THREE.ShaderChunk["common"],//名の通り、common
   THREE.ShaderChunk["skinning_pars_vertex"], //skinning頂点パーサー
   "void main() {",
      "vec3 transformed = vec3(position + normal * offset);",
      THREE.ShaderChunk["skinbase_vertex"],
      THREE.ShaderChunk["skinning_vertex"],
      THREE.ShaderChunk["project_vertex"],
   "}"
].join( "\n" ),
```

jsに直書きが気持ち悪いというかたは一旦devtool等にコードを出力してからコピペすると良い。

あとはPerlinNoiseなり、好きなvertexの処理を付け足して、最終的な処理を書く。

```glsl
//vertex-shader
	uniform float offset;
	#define PI 3.14159265359
	#define PI2 6.28318530718
	#define PI_HALF 1.5707963267949
	#define RECIPROCAL_PI 0.31830988618
	#define RECIPROCAL_PI2 0.15915494
	#define LOG2 1.442695
	#define EPSILON 1e-6
	#define saturate(a) clamp( a, 0.0, 1.0 )
	#define whiteCompliment(a) ( 1.0 - saturate( a ) )
	float pow2( const in float x ) { return x*x; }
	float pow3( const in float x ) { return x*x*x; }
	float pow4( const in float x ) { float x2 = x*x; return x2*x2; }
	float average( const in vec3 color ) { return dot( color, vec3( 0.3333 ) ); }
	highp float rand( const in vec2 uv ) {
		const highp float a = 12.9898, b = 78.233, c = 43758.5453;
		highp float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
		return fract(sin(sn) * c);
	}
	struct IncidentLight {
		vec3 color;
		vec3 direction;
		bool visible;
	};
	struct ReflectedLight {
		vec3 directDiffuse;
		vec3 directSpecular;
		vec3 indirectDiffuse;
		vec3 indirectSpecular;
	};
	struct GeometricContext {
		vec3 position;
		vec3 normal;
		vec3 viewDir;
	};
	vec3 transformDirection( in vec3 dir, in mat4 matrix ) {
		return normalize( ( matrix * vec4( dir, 0.0 ) ).xyz );
	}
	vec3 inverseTransformDirection( in vec3 dir, in mat4 matrix ) {
		return normalize( ( vec4( dir, 0.0 ) * matrix ).xyz );
	}
	vec3 projectOnPlane(in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {
		float distance = dot( planeNormal, point - pointOnPlane );
		return - distance * planeNormal + point;
	}
	float sideOfPlane( in vec3 point, in vec3 pointOnPlane, in vec3 planeNormal ) {
		return sign( dot( point - pointOnPlane, planeNormal ) );
	}
	vec3 linePlaneIntersect( in vec3 pointOnLine, in vec3 lineDirection, in vec3 pointOnPlane, in vec3 planeNormal ) {
		return lineDirection * ( dot( planeNormal, pointOnPlane - pointOnLine ) / dot( planeNormal, lineDirection ) ) + pointOnLine;
	}
	mat3 transpose( const in mat3 v ) {
		mat3 tmp;
		tmp[0] = vec3(v[0].x, v[1].x, v[2].x);
		tmp[1] = vec3(v[0].y, v[1].y, v[2].y);
		tmp[2] = vec3(v[0].z, v[1].z, v[2].z);
		return tmp;
	}

	#ifdef USE_SKINNING
		uniform mat4 bindMatrix;
		uniform mat4 bindMatrixInverse;
		#ifdef BONE_TEXTURE
			uniform sampler2D boneTexture;
			uniform int boneTextureWidth;
			uniform int boneTextureHeight;
			mat4 getBoneMatrix( const in float i ) {
				float j = i * 4.0;
				float x = mod( j, float( boneTextureWidth ) );
				float y = floor( j / float( boneTextureWidth ) );
				float dx = 1.0 / float( boneTextureWidth );
				float dy = 1.0 / float( boneTextureHeight );
				y = dy * ( y + 0.5 );
				vec4 v1 = texture2D( boneTexture, vec2( dx * ( x + 0.5 ), y ) );
				vec4 v2 = texture2D( boneTexture, vec2( dx * ( x + 1.5 ), y ) );
				vec4 v3 = texture2D( boneTexture, vec2( dx * ( x + 2.5 ), y ) );
				vec4 v4 = texture2D( boneTexture, vec2( dx * ( x + 3.5 ), y ) );
				mat4 bone = mat4( v1, v2, v3, v4 );
				return bone;
			}
		#else
			uniform mat4 boneMatrices[ MAX_BONES ];
			mat4 getBoneMatrix( const in float i ) {
				mat4 bone = boneMatrices[ int(i) ];
				return bone;
			}
		#endif
	#endif

	//NOISE SETTINGS
	attribute vec3 position2;

	uniform float time;
	uniform float ease_time;
	uniform float ease_time_max;
	uniform float radius;
	uniform float noise_a;
	uniform float noise_x;
	uniform float noise_y;
	uniform float noise_z;
	uniform float plane_noise_a;
	uniform float plane_noise_z;
	uniform float plane_noise_y;

	varying vec4 vPosition;
	varying vec2 vUv;
	varying mat4 vInvertMatrix;

	　...(めっちゃながいので中略）



	void main() {
	float step = circularInOut(clamp(ease_time, 0.0, ease_time_max) / ease_time_max);
	vec3 plane_position = (rotateMatrix(0.0, radians(-90.0), 0.0) * vec4(position2, 1.0)).xyz;
	vec3 ease_position = position * (1.0 - step) + plane_position * step;
	float noise = cnoise(
	vec3(
	ease_position.x * noise_x + time,
	ease_position.y * noise_y + time,
	ease_position.z * noise_z + time
	)
	);
	float noise2 = cnoise(
	vec3(
	ease_position.x + time,
	ease_position.y * plane_noise_y + time,
	ease_position.z * plane_noise_z + time
	)
	);
	mat4 scale_matrix = scaleMatrix(vec3(radius));
	vec4 scale_position = scale_matrix * vec4(ease_position, 1.0);
	vec4 noise_position = vec4(scale_position.xyz + vec3(
	position.x * noise * noise_a * (1.0 - step) + (plane_position.x + plane_noise_a * 2.0) * noise2 * step,
	position.y * noise * noise_a * (1.0 - step),
	position.z * noise * noise_a * (1.0 - step)
	), 1.0);

	vPosition = noise_position;
	vUv = uv;
	vInvertMatrix = inverse(scale_matrix * modelMatrix);	
	vec3 transformed = vec3(position + normal * offset);
	#ifdef USE_SKINNING
		mat4 boneMatX = getBoneMatrix( skinIndex.x );
		mat4 boneMatY = getBoneMatrix( skinIndex.y );
		mat4 boneMatZ = getBoneMatrix( skinIndex.z );
		mat4 boneMatW = getBoneMatrix( skinIndex.w );
	#endif
	#ifdef USE_SKINNING
		// vec4 skinVertex = bindMatrix * vec4( transformed, 1.0 );
		vec4 skinVertex = bindMatrix * noise_position;//skinの頂点にノイズを加える
		vec4 skinned = vec4( 0.0 );
		skinned += boneMatX * skinVertex * skinWeight.x;
		skinned += boneMatY * skinVertex * skinWeight.y;
		skinned += boneMatZ * skinVertex * skinWeight.z;
		skinned += boneMatW * skinVertex * skinWeight.w;
		skinned  = bindMatrixInverse * skinned;
	#endif

	#ifdef USE_SKINNING
		vec4 mvPosition = modelViewMatrix * skinned;
	#else
		vec4 mvPosition = modelViewMatrix * vec4( transformed, 1.0 );
	#endif
	gl_Position = projectionMatrix * mvPosition;

	}

```

ノイズの微調整、テクスチャ、アニメーションのスピードなどはモデルのインスタンス生成時にオブジェクトを参照させればよい。こんなかんじ

```js
 this.uniforms = {
 	time: {
 		type: 'f',
 		value: 0
 	},
 	ease_time: {
 		type: 'f',
 		value: 0
 	},
 	ease_time_max: {
 		type: 'f',
 		value: 1
 	},
 	radius: {
 		type: 'f',
 		value: this.radius
 	},
 	noise_a: {
 		type: 'f',
 		value: this.noise_a
 	},
 	noise_i: {
 		type: 'f',
 		value: this.noise_i
 	},
 	noise_x: {
 		type: 'f',
 		value: this.noise_x
 	},
 	noise_y: {
 		type: 'f',
 		value: this.noise_y
 	},
 	noise_z: {
 		type: 'f',
 		value: this.noise_z
 	},
 	plane_noise_a: {
 		type: 'f',
 		value: this.plane_noise_a
 	},
 	plane_noise_z: {
 		type: 'f',
 		value: this.plane_noise_z
 	},
 	plane_noise_y: {
 		type: 'f',
 		value: this.plane_noise_y
 	},
 	texture: {
 		type: 't',
 		value: new THREE.TextureLoader().load('./mytexture.jpg')
 	},
 	valid_tex: { //texture有効化
 		type: 'f',
 		value: 1
 	},
 	resolution: {
 		type: "v2",
 		value: new THREE.Vector2()
 	}
 }

```

ちなみにdat.guiなどで動的にパラメーターを調整したい場合はイベント処理と連動させてアップデートを行わなければダメ

```js
this.gui = new dat.GUI()
const noise_a = this.gui.add(self.params,"noise_a",-100,100)
.
.
.
    
noise_a.onChange((value)=>{
  this.myMesh.material.uniforms.noise_a.value = value
})
```


## 結果

動いた。
[うごいてるやつ](https://codepen.io/ykido/pen/QdZVMx)


### 参考

glsl辞典
http://ec.nikkeibp.co.jp/nsp/dl/08513/HTML5GAMES_AppC.pdf

stackoverflow
http://stackoverflow.com/questions/37606090/synching-two-meshes-one-with-shadermaterial/37625533#37625533