Three.jsのShaderMaterialでライティングを考慮したシェーダーを作成する方法

three.jsのShaderMaterialでは自分でシェーダーを作成できますが、ライテイングを考慮したサンプルを見たことがなかったので試してみました。

まず、three.jsで使われているシェーダーは[three.js/src/renderers/shaders](https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders)に断片化した形で保存されています。断片化されたコードは[WebGLProgram.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLProgram.js)で完全なGLSLコードにまとめられます。[WebGLProgram.js](https://github.com/mrdoob/three.js/blob/dev/src/renderers/webgl/WebGLProgram.js)はuniform変数やattribute変数なども自動で追加してくれます。

今回は`MeshPhongMaterial`で使用される[meshphong_vert.glsl](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphong_vert.glsl)と[meshphong_frag.glsl](https://github.com/mrdoob/three.js/blob/dev/src/renderers/shaders/ShaderLib/meshphong_frag.glsl)をもとに、最低限必要なものだけを残すことでライティングを憂慮したShaderMaterialを作成します。
以下のコードはthree.jsのr91で試しています。

Vertexシェーダーは次のようになります。`normal`や`position`、`modelViewMatrix`などの変数の宣言はthree.jsが自動で追加してくれます。
やっていることは単純に頂点位置を座標変換しているだけです。

```glsl:vertexShader
varying vec3 vViewPosition;
varying vec3 vNormal;

void main() {
  vNormal = normalMatrix * normal;
  vViewPosition = -(modelViewMatrix * vec4(position, 1.0)).xyz;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
```

Fragmentシェーダーは以下のようになっています。`#include`はGLSLの文法ではなくthree.js独自のもので、[ShaderChunk](https://github.com/mrdoob/three.js/tree/dev/src/renderers/shaders/ShaderChunk)の対応したものに置き換えられます。
`diffuseColor`に指定した色がマテリアルのベースの色になります。

```glsl:fragmentShader
#include <common>
#include <bsdfs>
#include <lights_pars_begin>
#include <lights_pars_maps>
#include <lights_phong_pars_fragment>

vec3 emissive = vec3(0.0);
vec3 specular = vec3(1.0);
float shininess = 8.0;

void main() {
  vec3 diffuseColor = vec3(0.5, 0.5, 1.0);
  ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));

  #include <specularmap_fragment>
  #include <normal_fragment_begin>
  #include <normal_fragment_maps>
  #include <lights_phong_fragment>
  #include <lights_fragment_begin>
  #include <lights_fragment_maps>
  #include <lights_fragment_end>

  vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + emissive;
  gl_FragColor = vec4(outgoingLight, 1.0);
}
```


ShaderMaterialには以下のようにライトに関するuniforms変数が必要になります。

```js
const material = new THREE.ShaderMaterial({
  uniforms: {
    ambientLightColor: {value: null},
    directionalLights: {value: null},
    spotLights: {value: null},
    rectAreaLights: {value: null},
    pointLights: {value: null},
    hemisphereLights: {value: null},
    directionalShadowMap: {value: null},
    directionalShadowMatrix: {value: null},
    spotShadowMap: {value: null},
    spotShadowMatrix: {value: null},
    pointShadowMap: {value: null},
    pointShadowMatrix: {value: null},
  },
  vertexShader: document.getElementById('vertexShader').textContent,
  fragmentShader: document.getElementById('fragmentShader').textContent
});
material.lights = true;
```

これを利用すると次のようにライティングを考慮した上でプロシージャルなテクスチャを生成することができます。
https://aadebdeb.github.io/study-three.js/shadermaterial-with-lighting.html

![f2c2dea9-771f-1e9c-d9c5-f04aeccc90a4.gif](https://qiita-image-store.s3.amazonaws.com/0/61569/0485be80-5fa1-0ac6-4e50-ac78f2a985d8.gif)


ソースコード全体は以下のようになっています。

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="urf-8">
    <title>Sample of ShaderMaterial with lightings</title>
  </head>
  <body>
    <script src="./three.js"></script>
    <script id="vertexShader" type="x-shader/x-vertex">
      varying vec3 vModelPosition;
      varying vec3 vViewPosition;
      varying vec3 vNormal;

      void main() {
        vNormal = normalMatrix * normal;
        vModelPosition = (modelMatrix * vec4(position, 1.0)).xyz;
        vViewPosition = -(modelViewMatrix * vec4(position, 1.0)).xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    </script>
    <script id="fragmentShader" type="x-shader/x-fragment">
      #include <common>
      #include <bsdfs>
      #include <lights_pars_begin>
      #include <lights_pars_maps>
      #include <lights_phong_pars_fragment>

      uniform float time;
      varying vec3 vModelPosition;

      vec3 emissive = vec3(0.0);
      vec3 specular = vec3(1.0);
      float shininess = 8.0;

      void main() {
        float d = length(vec3(vModelPosition.xz, 0.0));
        vec3 diffuseColor = mix(vec3(0.1, 0.3, 0.05), vec3(0.9, 0.7, 0.2), pow(abs(sin(d * 0.3 - time * 5.0)), 12.0));
        ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));

        #include <specularmap_fragment>
        #include <normal_fragment_begin>
      	#include <normal_fragment_maps>
        #include <lights_phong_fragment>
      	#include <lights_fragment_begin>
      	#include <lights_fragment_maps>
      	#include <lights_fragment_end>

        vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + emissive;
        gl_FragColor = vec4(outgoingLight, 1.0);
      }
    </script>
    <script>
      const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
      camera.position.y = 120;
      camera.lookAt(0, 0, 0);

      const scene = new THREE.Scene();
      scene.background = new THREE.Color(0x333333);

      const material = new THREE.ShaderMaterial({
        uniforms: {
          ambientLightColor: {value: null},
          directionalLights: {value: null},
          spotLights: {value: null},
          rectAreaLights: {value: null},
          pointLights: {value: null},
          hemisphereLights: {value: null},
          directionalShadowMap: {value: null},
          directionalShadowMatrix: {value: null},
          spotShadowMap: {value: null},
          spotShadowMatrix: {value: null},
          pointShadowMap: {value: null},
          pointShadowMatrix: {value: null},
          time: {value: null},
        },
        vertexShader: document.getElementById('vertexShader').textContent,
        fragmentShader: document.getElementById('fragmentShader').textContent
      });
      material.lights = true;

      const box = new THREE.Mesh(new THREE.BoxBufferGeometry(20, 30, 20), material);
      box.position.set(20, 15, 15);
      box.rotation.y = Math.PI / 4.0;
      scene.add(box);

      const ground = new THREE.Mesh(new THREE.BoxBufferGeometry(100, 300, 100), material);
      ground.position.y = -150;
      scene.add(ground);

      const sphere = new THREE.Mesh(new THREE.SphereBufferGeometry(15, 64, 64), material);
      sphere.position.set(-10, 7.5, -25);
      scene.add(sphere);

      const torus = new THREE.Mesh(new THREE.TorusKnotBufferGeometry(10, 4, 128, 24), material);
      torus.position.set(-20, 15, 20);
      scene.add(torus);

      scene.add(new THREE.AmbientLight(0x666666));
      const light = new THREE.DirectionalLight(0xffffff, 1);
      light.position.set(1, 1, 1);
      scene.add(light);

      const renderer = new THREE.WebGLRenderer();
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);

      let time = 0.0;
      animate();
      function animate() {
        requestAnimationFrame(animate);

        time += Math.PI * 2.0 / 300;
        material.uniforms.time.value = time;
        camera.position.x = 150.0 * Math.cos(time);
        camera.position.z = 150.0 * Math.sin(time);
        camera.lookAt(0, 0, 0);
        camera.updateProjectionMatrix();
        renderer.render(scene, camera);
      }
    </script>
  </body>
</html>
```