three.jsのShaderMaterialでシーン内のライトを使用する

three.jsの[ShaderMaterial](https://threejs.org/docs/#api/en/materials/ShaderMaterial)または[RawShaderMaterial](https://threejs.org/docs/#api/en/materials/RawShaderMaterial)を使うと自分で書いたシェーダーでシェーディングを行うことができます。ShaderMaterialを使うと組み込みのuniformsとattributesを使うことができますが、RawShaderMaterialにはそのようなサポートがありません。ShaderMaterialで使用できるuniformsやattributesは[ここ](https://threejs.org/docs/#api/en/renderers/webgl/WebGLProgram)にまとまっています。

今回はShaderMaterialを使用して、シーン内のLight(DirectionalLight、PointLightなど)を考慮したライティングをしたいと思います。


ランバート反射を実装したMeshLambertMaterialは[ドキュメント](https://threejs.org/docs/#api/en/materials/MeshLambertMaterial)に書いてあるように、頂点単位でライティングを行うグーローシェーディングになっています。今回はサンプルとして、ShaderMaterialを使ってピクセル単位でランバート反射モデルによりライティングを行うマテリアルを作ります。

使用するthree.jsのバージョンはr109です。

---

まずShaderMaterialの基本的な使用方法を確認します。以下はオブジェクトを単色で塗りつぶすだけのシンプルなShaderMaterialです。`vertexShader`は頂点シェーダー、`fragmentShader`はフラグメントシェーダーのソースコードで、`uniforms`はuniforms変数の値になります。`projectionMatrix`や`modelViewMatrix`、`position`は組み込みuniforms、組み込みattributesで、これらの宣言はthree.js側で挿入されるためソースコードには不要です。

```js
const material = new THREE.ShaderMaterial({
  vertexShader: `
void main(void) {
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
  fragmentShader: `
uniform vec3 color;
void main(void) {
  gl_FragColor = vec4(color, 1.0);
}
`,
  uniforms: {
    color: { value: new THREE.Color(0xff0000) },
  },
});
```

基本的な使い方を確認したところで、ShaderMaterialでライティングをしていきます。
まずはShaderMaterialの[lights](https://threejs.org/docs/#api/en/materials/ShaderMaterial.lights)プロパティにtrueを設定して、レンダリング時にシーン内のライトがuniformsとしてシェーダーに渡るようにします。

```js
const material = new THREE.ShaderMaterial({
  ...
  lights: true,
});
```

しかしこれだけではShaderMaterialのuniformsプロパティにライトの情報がないため、実行時にエラーになります。既存のマテリアルで使用されるuniformsの情報は[ShaderLib.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib.js)にまとまっており、例えばMeshLambertMaterialは以下のようになっています。

```js:/src/renderers/shaders/ShaderLib.js
	lambert: {

		uniforms: mergeUniforms( [
			UniformsLib.common,
			UniformsLib.specularmap,
			UniformsLib.envmap,
			UniformsLib.aomap,
			UniformsLib.lightmap,
			UniformsLib.emissivemap,
			UniformsLib.fog,
			UniformsLib.lights,
			{
				emissive: { value: new Color( 0x000000 ) }
			}
		] ),

		vertexShader: ShaderChunk.meshlambert_vert,
		fragmentShader: ShaderChunk.meshlambert_frag

	},
```

これを見ると`UniformLib.lights`というものがあり、ここにライト関係のuniformsがありそうです。[UniformLib.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/UniformsLib.js)内の`UniformLib.lights`は次のようになっており、確かにlightsの情報があります。

```js:/src/renderers/shaders/UniformsLib.js
	lights: {

		ambientLightColor: { value: [] },

		lightProbe: { value: [] },

		directionalLights: { value: [], properties: {
			direction: {},
			color: {},

			shadow: {},
			shadowBias: {},
			shadowRadius: {},
			shadowMapSize: {}
		} },

		directionalShadowMap: { value: [] },
		directionalShadowMatrix: { value: [] },

		spotLights: { value: [], properties: {
			color: {},
			position: {},
			direction: {},
			distance: {},
			coneCos: {},
			penumbraCos: {},
			decay: {},

			shadow: {},
			shadowBias: {},
			shadowRadius: {},
			shadowMapSize: {}
		} },

		spotShadowMap: { value: [] },
		spotShadowMatrix: { value: [] },

		pointLights: { value: [], properties: {
			color: {},
			position: {},
			decay: {},
			distance: {},

			shadow: {},
			shadowBias: {},
			shadowRadius: {},
			shadowMapSize: {},
			shadowCameraNear: {},
			shadowCameraFar: {}
		} },

		pointShadowMap: { value: [] },
		pointShadowMatrix: { value: [] },

		hemisphereLights: { value: [], properties: {
			direction: {},
			skyColor: {},
			groundColor: {}
		} },

		// TODO (abelnation): RectAreaLight BRDF data needs to be moved from example to main src
		rectAreaLights: { value: [], properties: {
			color: {},
			position: {},
			width: {},
			height: {}
		} }

	},
```

ShaderMaterialでもライトのuniformsの使うために、`uniforms`プロパティで`UniformsLib.lights`を使用するようにします。`UniformUtils.merge`はthree.js内コードで使用されている`mergeUniforms`メソッドと同じもので、複数のオブジェクトに分かれているuniformsを一つのオブジェクトにまとめてくれます。

```js
const material = new THREE.ShaderMaterial({
  ...
  uniforms: THREE.UniformsUtils.merge([
    THREE.UniformsLib.lights,
    { ... } // 任意のuniforms
  ]),
  lights: true,
});
```

uniformsとしてライトの情報をシェーダーで渡せるようになったので、次はシェーダー側のソースコードを書いていきたいと思います。

まずは参考として、MeshLambertMaterialのシェーダーについて確認していきます。頂点シェーダーは[meshlambert_vert.glsl.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshlambert_vert.glsl.js)に、フラグメントシェーダーは[meshlambert_frag.glsl.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshlambert_frag.glsl.js)にソースコードがあります。three.jsではシェーダーのコードを機能単位で分けており、それを`#include`することで一つのシェーダーを生成しています。MeshLambertMaterialはグーローシェーディングなので、ライティングの処理は頂点シェーダーにあることになります。
まずライトの情報の読み込みですが、次のように`#include <lights_pars_begin>`([lights_pars_begin.glsl.js](https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js))で行われています。

```glsl:/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js
...
uniform vec3 ambientLightColor;
...
uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];
...
uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
...
```

また、ライティングは次のように`#include <lights_pars_begin>`([lights_lambert_vertex.glsl.js](https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/lights_lambert_vertex.glsl.js))で行われています。

```glsl:/src/renderers/shaders/ShaderChunk/lights_lambert_vertex.glsl.js
// ソースコードを一部のみ抜粋
...
#if NUM_POINT_LIGHTS > 0
	#pragma unroll_loop
	for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
		getPointDirectLightIrradiance( pointLights[ i ], geometry, directLight );
		dotNL = dot( geometry.normal, directLight.direction );
		directLightColor_Diffuse = PI * directLight.color;
		vLightFront += saturate( dotNL ) * directLightColor_Diffuse;
		#ifdef DOUBLE_SIDED
			vLightBack += saturate( -dotNL ) * directLightColor_Diffuse;
		#endif
	}
#endif
...
```

以上のことから、フラグメントシェーダーで`#include <lights_pars_begin>`によりライトの情報をuniformsで読み込み、main関数内で`#include <lights_lambert_vertex>`相当の処理を行えばピクセル単位のランバートシェーディングができそうです。

```js
const material = new THREE.ShaderMaterial({
  vertexShader: `...`,
  fragmentShader: `
...
#include <lights_pars_begin>
...
void main(void) {
...
// #include <lights_lambert_vertex> 相当の処理
...
}
`,
  uniforms: THREE.UniformsUtils.merge([
    THREE.UniformsLib.lights,
    { ... } // 任意のuniforms
  ]),
  lights: true,
});
```

---

というわけで、以上のことを踏まえてShaderMaterialでシーン内のライトを考慮したピクセル単位のランバート反射モデルを実装すると次のようになります。MeshLambertMaterialの[color](https://threejs.org/docs/#api/en/materials/MeshLambertMaterial.color)プロパティと[emissive](https://threejs.org/docs/#api/en/materials/MeshLambertMaterial.emissive)プロパティだけを考慮した簡略化バージョンです。

```js
const material = new THREE.ShaderMaterial({
  vertexShader: `
varying vec3 vViewPosition;
varying vec3 vNormal;

void main(void) {
  vNormal = normalMatrix * normal;
  vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
  vViewPosition = mvPosition.xyz;
  gl_Position = projectionMatrix * mvPosition;
}
  `,
  fragmentShader: `
uniform vec3 diffuse;
uniform vec3 emissive;

varying vec3 vViewPosition;
varying vec3 vNormal;

#include <common>
#include <bsdfs>
#include <lights_pars_begin>

void main(void) {
  vec3 mvPosition = vViewPosition;
  vec3 transformedNormal = vNormal;
  
  // ref: https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/lights_lambert_vertex.glsl.js
  GeometricContext geometry;
  geometry.position = mvPosition.xyz;
  geometry.normal = normalize(transformedNormal);
  geometry.viewDir = (normalize(-mvPosition.xyz));
  vec3 lightFront = vec3(0.0);
  vec3 indirectFront = vec3(0.0);
  IncidentLight directLight;
  float dotNL;
  vec3 directLightColor_Diffuse;
  #if NUM_POINT_LIGHTS > 0
  #pragma unroll_loop
  for ( int i = 0; i < NUM_POINT_LIGHTS; i ++ ) {
    getPointDirectLightIrradiance(pointLights[ i ], geometry, directLight);
    dotNL = dot(geometry.normal, directLight.direction);
    directLightColor_Diffuse = PI * directLight.color;
    lightFront += saturate(dotNL) * directLightColor_Diffuse;
	}
  #endif
  #if NUM_SPOT_LIGHTS > 0
  #pragma unroll_loop
  for ( int i = 0; i < NUM_SPOT_LIGHTS; i ++ ) {
    getSpotDirectLightIrradiance(spotLights[ i ], geometry, directLight);
    dotNL = dot(geometry.normal, directLight.direction);
    directLightColor_Diffuse = PI * directLight.color;
    lightFront += saturate(dotNL) * directLightColor_Diffuse;
  }
  #endif
  #if NUM_DIR_LIGHTS > 0
  #pragma unroll_loop
  for ( int i = 0; i < NUM_DIR_LIGHTS; i ++ ) {
    getDirectionalDirectLightIrradiance(directionalLights[ i ], geometry, directLight);
    dotNL = dot(geometry.normal, directLight.direction);
    directLightColor_Diffuse = PI * directLight.color;
    lightFront += saturate(dotNL) * directLightColor_Diffuse;
  }
  #endif
  #if NUM_HEMI_LIGHTS > 0
  #pragma unroll_loop
  for ( int i = 0; i < NUM_HEMI_LIGHTS; i ++ ) {
    indirectFront += getHemisphereLightIrradiance( hemisphereLights[ i ], geometry );
  }
  #endif

  // ref: https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshlambert_frag.glsl.js
  vec4 diffuseColor = vec4(diffuse, 1.0);
  ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
  vec3 totalEmissiveRadiance = emissive;
  reflectedLight.indirectDiffuse = getAmbientLightIrradiance(ambientLightColor);
  reflectedLight.indirectDiffuse += indirectFront;
  reflectedLight.indirectDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
  reflectedLight.directDiffuse = lightFront;
  reflectedLight.directDiffuse *= BRDF_Diffuse_Lambert(diffuseColor.rgb);
  vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + totalEmissiveRadiance;
  gl_FragColor = vec4(outgoingLight, diffuseColor.a);
}
  `,
  uniforms: THREE.UniformsUtils.merge([
    THREE.UniformsLib.lights,
    {
      'diffuse': { value: new THREE.Color(0xffffff) },
      'emissive': { value: new THREE.Color(0x000000) },
    }
  ]),
  lights: true,
});
```

デモです。
https://aadebdeb.github.io/Sample_Three_ShaderMaterial_Lighting/index.html
(ソースコード: https://github.com/aadebdeb/Sample_Three_ShaderMaterial_Lighting)
![shadermaterial_lighting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61569/ef467ec8-4506-a6ce-c8bb-671ab0df92b0.png)
手前側がランバート反射モデルを実装したShaderMaterial、奥側がMeshLambertMaterialを使用しています。同じようにシェーディングされていますが、ShaderMaterialのほうがピクセル単位でシェーディングしているので綺麗にレンダリングされています。