LoginSignup
17
14

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-04-25

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

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

今回はMeshPhongMaterialで使用されるmeshphong_vert.glslmeshphong_frag.glslをもとに、最低限必要なものだけを残すことでライティングを憂慮したShaderMaterialを作成します。
以下のコードはthree.jsのr91で試しています。

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

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の対応したものに置き換えられます。
diffuseColorに指定した色がマテリアルのベースの色になります。

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変数が必要になります。

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

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

<!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>
17
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
14