Three.jsにて自作シェーダーを試す際のサンプル Part.1

Three.jsでカスタムシェーダーを扱う際の自分用メモ。
下の記事とか非常に参考になる 。
https://qiita.com/mebiusbox2/items/8a4734ab5b0854528789

Three.jsのライトとかそのヘルパー凄い使いやすいんだけど、その情報を自分のシェーダーに組み込む方法が分からなかったんで、そこらへん含めて調べてみた。
結論を言うと、以下のようなコードを基本にするとかなり楽に色々実験できそう。

ファイル構成main.html
main.js
js/
 └─three.js
   OrbitControls.js
   LoaderSupport.js
   OBJLoader2.js

サンプル : http://nktk-tech.com/example/three-template1/main.html

ソースコード

main.html<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8"/>
    <script src="js/three.js"></script>
    <script src="js/LoaderSupport.js"></script>
    <script src="js/OBJLoader2.js"></script>
    <script src="js/OrbitControls.js"></script>
    <script src="main.js"></script>

    <script id="vs" type="x-shader/x-vertex">
        // PIの定義などがあり、ライト情報を使うのに必要
        #include <common>
        // punctualLightIntensityToIrradianceFactor関数がライトの定義の読み込みに必要
        #include <bsdfs>
        // ライトの構造体、uniform変数などが定義されている
        #include <lights_pars_begin>

        varying vec4 fragColor;

        void main() {
            vec4 tempFragColor = vec4(0.0, 0.0, 0.0, 0.0);
            for (int i = 0; i < NUM_POINT_LIGHTS; i++) {
                vec4 vertexToLight = normalize(vec4(pointLights[i].position, 1.0) - modelViewMatrix * vec4(position, 1.0));
                tempFragColor += vec4(pointLights[i].color, 1.0) * max(dot(vertexToLight.xyz, normalMatrix * normal), 0.0);
            }
            fragColor = tempFragColor;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
         }
    </script>
    <script id="fs" type="x-shader/x-fragment">
        varying vec4 fragColor;
        void main() {
            gl_FragColor = fragColor;
        }
    </script>
</head>

<body>
    <canvas id="myCanvas"></canvas>
</body>

</html>
main.jswindow.addEventListener('load', init);

function init() {

    // サイズを指定
    const width = 960;
    const height = 540;

    // レンダラーを作成
    const renderer = new THREE.WebGLRenderer({
        canvas: document.querySelector('#myCanvas')
    });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(width, height);
    renderer.setClearColor(new THREE.Color( 0.5, 0.5, 0.5 ))

    // シーンを作成
    const scene = new THREE.Scene();

    // カメラを作成
    const camera = new THREE.PerspectiveCamera(45, width / height);
    camera.position.set(0, 5, +10);

    // マウスドラッグによるコントーロールを有効化
    const controls = new THREE.OrbitControls( camera );

    // 点光源追加
    const light = new THREE.PointLight( 0xffffee );
    light.position.set( 5, 3, 5 );
    scene.add( light );

    const sphereSize = 1;
    const pointLightHelper = new THREE.PointLightHelper( light, sphereSize );
    scene.add( pointLightHelper );

    // モデルのロードが終わった際のコールバック
    const callbackOnLoad = (event) => {
        let rootNode = event.detail.loaderRootNode;
        rootNode.children[0].material = new THREE.ShaderMaterial({
            uniforms: THREE.UniformsLib['lights'],
            vertexShader: document.getElementById('vs').textContent,
            fragmentShader: document.getElementById('fs').textContent,
            lights: true,
        });
        scene.add(rootNode);
        renderer.render(scene, camera);
        tick();
    };

    // .obj形式のモデルをロード
    const objLoader = new THREE.OBJLoader2();
    objLoader.setUseIndices(true);
    objLoader.load( 'model/teapot.obj', callbackOnLoad, null, null, null, false );

    // 毎フレーム時に実行されるループイベント
    function tick() {
        controls.update();
        renderer.render(scene, camera); // レンダリング
        requestAnimationFrame(tick);
    }

}

解説

Three.js初期化処理(main.js 6~22行)

main.js抜粋const width = 960;
const height = 540;

// レンダラーを作成
const renderer = new THREE.WebGLRenderer({
    canvas: document.querySelector('#myCanvas')
});
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(width, height);
renderer.setClearColor(new THREE.Color( 0.5, 0.5, 0.5 ))

// シーンを作成
const scene = new THREE.Scene();

// カメラを作成
const camera = new THREE.PerspectiveCamera(45, width / height);
camera.position.set(0, 5, +10);

初期化したり、カメラ作成したり。
公式のドキュメントが分かりやすい。
https://threejs.org/docs/index.html#manual/en/introduction/Creating-a-scene

マウスによるカメラのコントロールを追加(main.js 25行)

main.js抜粋// マウスドラッグによるコントーロールを有効化
const controls = new THREE.OrbitControls( camera );

作成したカメラにてTHREE.OrbitControlsを作成すると、マウスやマウスホイールの操作にて自動でカメラの位置を変更してくれるようになる。ちょっとした確認に便利。

光源を追加(main.js 27~34行)

main.js抜粋// 点光源追加
const light = new THREE.PointLight( 0xffffee );
light.position.set( 5, 3, 5 );
scene.add( light );

const sphereSize = 1;
const pointLightHelper = new THREE.PointLightHelper( light, sphereSize );
scene.add( pointLightHelper );

点光源を追加している。
光源には対応するHlperクラス(PointLightの場合はPointLightHelper)があり、それをシーンに配置すると光源の位置を可視化することが出来る。
スポットライトなんかの場合は、光線が届く範囲が可視化されて非常に便利。
参考 : https://threejs.org/docs/index.html#api/en/helpers/PointLightHelper

オブジェクトをロードし、マテリアルを設定する(main.js 36~53行 )

main.js抜粋// モデルのロードが終わった際のコールバック
const callbackOnLoad = (event) => {
    let rootNode = event.detail.loaderRootNode;
    rootNode.children[0].material = new THREE.ShaderMaterial({
        uniforms: THREE.UniformsLib['lights'],
        vertexShader: document.getElementById('vs').textContent,
        fragmentShader: document.getElementById('fs').textContent,
        lights: true,
    });
    scene.add(rootNode);
    renderer.render(scene, camera);
    tick();
};

// .obj形式のモデルをロード
const objLoader = new THREE.OBJLoader2();
objLoader.setUseIndices(true);
objLoader.load( 'model/teapot.obj', callbackOnLoad, null, null, null, false );

OBJLoader2にて、.objファイルを読み込んでいる。 objLoader.setUseIndices(true);と設定することにより、元ファイルのインデックスの情報を残したままファイルを読み込んでくれる。 OBJLoader2が法線を自動計算する際に、これをTrueにしなかったら面単位の法線を計算してしまうため、頂点単位の法線が欲しい場合はTrueにしておく。元ファイルに法線の情報があるならどうでもいいのかな。
.loadメソッドの引数については以下を参照。
https://threejs.org/docs/index.html#examples/loaders/OBJLoader2.load

読み込みが終わった際のコールバック関数にて、オブジェクトに自作のシェーダーをアタッチしている。

main.js抜粋
let rootNode = event.detail.loaderRootNode;
rootNode.children[0].material = new THREE.ShaderMaterial({
    uniforms: THREE.UniformsLib['lights'],
    vertexShader: document.getElementById('vs').textContent,
    fragmentShader: document.getElementById('fs').textContent,
    lights: true,
});

ShaderMaterialにて、自作のシェーダーを用いたマテリアルを作成し、オブジェクトにアタッチしている。
ShaderMaterial に渡すオブジェクトの、「vertexShader」と「fragmentShader」プロパティには、自作したシェーダーを文字列として設定する。
また自作シェーダー内で、Three.jsの光源情報を用いたい場合は、「uniforms」プロパティにTHREE.UniformsLib[‘lights’]プロパティを設定する必要がある。 THREE.UniformsLib[‘lights’] には、光源の情報がuniformsプロパティに渡せる形で保存されているっぽい。
光源だけじゃなく、fogとかshadowmapとかもThree.jsのものを使いたい場合は、同様にuniformsプロパティに情報を追加する必要があるが、その際にはTHREE.UniformsUtils.mergeが使えるっぽい。
参考 : https://stackoverflow.com/questions/30287170/combining-shaders-in-three-js
あとはlightsプロパティをtrueにしてやれば、自作シェーダーからThree.jsの光源情報を取得できるようになる。

アニメーションの設定(main.js 56~60行 )

main.js抜粋// 毎フレーム時に実行されるループイベントです
function tick() {
    controls.update();
    renderer.render(scene, camera); // レンダリング
    requestAnimationFrame(tick);
}

requestAnimationFrame関数にて、一定時間ごとに再レンダリングすることでアニメーションを行う。
毎フレームごとに作成したOrbitControlsをupdateしてやらないとマウスでの操作ができないので注意。

シェーダーの解説

今回のサンプルコードのようにShaderMaterialを用いると、Three.jsがシェーダーに自動で変数や関数を新たに付与する。基本的な行列や、頂点の位置、法線などは、Three.jsが付与した変数から値を取得する。
参考 : https://qiita.com/gam0022/items/1fe17e93f0cd0432a8b0
自動でコードが付与されるのが嫌ならRawShaderMaterialを使うのがいいかも。

VertexShaderの解説(main.html 12~31行 )

main.html抜粋<script id="vs" type="x-shader/x-vertex">
    // PIの定義などがあり、ライト情報を使うのに必要
    #include <common>
    // punctualLightIntensityToIrradianceFactor関数がライトの定義の読み込みに必要
    #include <bsdfs>
    // ライトの構造体、uniform変数などが定義されている
    #include <lights_pars_begin>

    varying vec4 fragColor;

    void main() {
        vec4 tempFragColor = vec4(0.0, 0.0, 0.0, 0.0);
        for (int i = 0; i < NUM_POINT_LIGHTS; i++) {
            vec4 vertexToLight = normalize(vec4(pointLights[i].position, 1.0) - modelViewMatrix * vec4(position, 1.0));
            tempFragColor += vec4(pointLights[i].color, 1.0) * max(dot(vertexToLight.xyz, normalMatrix * normal), 0.0);
        }
        fragColor = tempFragColor;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position,1.0);
        }
</script>

#includeの構文はGLSLにはなく、Three.jsが拡張した構文のようだ。
これはShaderChunkと呼ばれるThree.jsが定義したGLSLのコード片を読み込むために使われ、シェーダのコンパイル前にGLSLのコードに置き換わるようだ。
ShaderChunkの内容については下記リポジトリを参照。
https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderChunk

今回はライト情報が欲しいため、その構造体とuniform変数が定義されているlights_pars_beginをincludeしている。また、lights_pars_begin内でcommonとbsdfsのShaderChunkの変数が参照されているためそれらも併せてincludeしている。
使わない関数の定義により、シェーダーが重くなるのが嫌なら、ShaderChunkから必要なコードだけ抜き出して自分のシェーダーにコピペしてもいいかも。
参考 : https://stackoverflow.com/questions/35596705/using-lights-in-three-js-shader

今回使用したTHREE.PointLightの場合、uniform変数は
uniform PointLight pointLights[ NUM_POINT_LIGHTS ];
として定義される。
NUM_POINT_LIGHTSは、jsで作成したPointLight の数のこと。
この光源の位置は、pointLights[i]. position、色は pointLights[i].colorとアクセスできる。
他の要素や、他の光源の場合のuniform変数などについては以下のコードを参照。
https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/lights_pars_begin.glsl.js

pointLightsのpositionには視点座標系の位置が入っていることに注意する。
自分はこれに気が付かず、 pointLightsのpositionにmodelView行列を乗算してしまって混乱した。

また、ShaderChunkのコードを見るとわかるが、pointLightsはjsにて点光源を作成していない場合には定義されない (別の光源でも同様) 。なので、このサンプルコードの場合、点光源を削除するとエラーが発生してしまう。
そのためpointLights変数を用いる際は、#ifディレクティブを使って、点光源が定義されている場合とされていない場合とで分岐させた方がいいかも。
コードは上のリンクのlights_pars_begin.glsl.jsのものが参考になる。

また、src/renderers/shaders/ShaderLib/にThree.jsがShaderChunkを利用して作ったシェーダーのソースがあるので、ShaderChunkを利用してシェーダーを書く際はここら辺のコードも参考になりそう。
参考 : https://github.com/mrdoob/three.js/tree/master/src/renderers/shaders/ShaderLi

Part.2はこちら

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です