ShaderLib、ShaderChunk、UniformsLibを使ってTHREE.ShaderMaterialの実装を端折る - Overpass APIとthree.jsで地図を3D表示(6)

公開:2017-05-17 10:00
更新:2020-02-15 04:37
カテゴリ:overpass apiとthree.jsで地図を3d表示

THREE.ShaderMaterialを試すための環境を作り、試す。

アイデアを実現するための検証環境を作って試行錯誤した。結果、所望の動作をさせることができた。テクスチャが伸縮することなく、ExtrudeBufferGeometryのamountに応じてテクスチャが貼り付けることができている。

  

階数とテクスチャー番号のアトリビュートを用意して、それを元にフラグメントシェーダーで階数に応じてうまくテクスチャを貼り付けるようにした。ただテクスチャを貼り付けるところ以外はMeshPhongMaterialのままで動いてほしい。ライティングとか座標変換とかね。こういうのを一から実装するのは骨が折れるし、Three.jsを使う意味があまりなくなってしまう。私の場合さらに3Dビギナーであるため、phongシェーディングをいちから実装することはすぐにできない。一からシェーダーを全部書くには私にはヘビーすぎる。何とか最低限コードを書くだけで所望の処理を実現できないだろうか。

調べた結果、ShaderLibShaderChunkを使えばできそうだということが分かった。そして実際にそれを使ってコードを書いた。

ShaderLibとShaderChunk、UniformLib

シェーダーはマテリアルを指定するときに自動的に決まる。three.jsは描画時にマテリアルに適したシェーダーをShaderLibから選択してレンダリングを行う。

ShaderLibはいろいろなシェーダーのライブラリ、ShaderChunkはシェーダーコードの断片、UniformsLibuniform変数のテンプレートである。ShaderLibはShaderChunkとUniformsLibを組み合わせること+αで作られている。

ShaderLibはシェーダー名のプロパティを持つ。その中身はuniformsvertexShaderfragmentShaderの3つのプロパティである。例えばphongシェーダーは以下の定義となっている。

var ShaderLib = {
.
.
.
phong: {

        uniforms: UniformsUtils.merge( [
            UniformsLib.common,
            UniformsLib.aomap,
            UniformsLib.lightmap,
            UniformsLib.emissivemap,
            UniformsLib.bumpmap,
            UniformsLib.normalmap,
            UniformsLib.displacementmap,
            UniformsLib.gradientmap,
            UniformsLib.fog,
            UniformsLib.lights,
            {
                emissive: { value: new Color( 0x000000 ) },
                specular: { value: new Color( 0x111111 ) },
                shininess: { value: 30 }
            }
        ] ),

        vertexShader: ShaderChunk.meshphong_vert,
        fragmentShader: ShaderChunk.meshphong_frag

    },
.
.

uniformsプロパティにはそのシェーダーで使用するuniform変数を定義する。この変数定義はUniformsLibでテンプレート化されており、これにより定義の簡略化と名称の共通化は果たしている。

たとえばUniformsLib.commonの内容は以下となっている。

var UniformsLib = {

    common: {

        diffuse: { value: new Color( 0xeeeeee ) },
        opacity: { value: 1.0 },

        map: { value: null },
        offsetRepeat: { value: new Vector4( 0, 0, 1, 1 ) },

        specularMap: { value: null },
        alphaMap: { value: null },

        envMap: { value: null },
        flipEnvMap: { value: - 1 },
        reflectivity: { value: 1.0 },
        refractionRatio: { value: 0.98 }

    },
  .
  .
  .

オリジナルのシェーダーを実装する場合はできる限りUniformsLibで提供されているuniform変数を使うべきだろう。上のコードではUniformUtils.merge()を使って、UniformsLibを1つにまとめている。

vertexShaderには頂点シェーダーを指定する。上のphongシェーダーではShaderChunk.meshbasic_vertシェーダーが指定されている。このシェーダーの内容は以下のとおりである。

#define PHONG

varying vec3 vViewPosition;

#ifndef FLAT_SHADED

    varying vec3 vNormal;

#endif

#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

void main() {

    #include <uv_vertex>
    #include <uv2_vertex>
    #include <color_vertex>

    #include <beginnormal_vertex>
    #include <morphnormal_vertex>
    #include <skinbase_vertex>
    #include <skinnormal_vertex>
    #include <defaultnormal_vertex>

#ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED

    vNormal = normalize( transformedNormal );

#endif

    #include <begin_vertex>
    #include <displacementmap_vertex>
    #include <morphtarget_vertex>
    #include <skinning_vertex>
    #include <project_vertex>
    #include <logdepthbuf_vertex>
    #include <clipping_planes_vertex>

    vViewPosition = - mvPosition.xyz;

    #include <worldpos_vertex>
    #include <envmap_vertex>
    #include <shadowmap_vertex>
    #include <fog_vertex>

}

なんとコードの中身は#includeの塊である。#includeで指定されている値はShaderChunkのプロパティである。
うむむ。。なんだこの#include、こんなディレクティブWebGLのシェーダー言語の仕様にあったっけ??としばし悩む。

実は「ない」。これはthree.jsだけで使える。描画時に#includeShaderChunkのコード片が展開される。 そのコードはWebGLProgram.jsにあるparseIncludes()関数である。

function parseIncludes( string ) {

    var pattern = /^[ \t]*#include +<([\w\d.]+)>/gm;

    function replace( match, include ) {

        var replace = ShaderChunk[ include ];

        if ( replace === undefined ) {

            throw new Error( 'Can not resolve #include <' + include + '>' );

        }

        return parseIncludes( replace );

    }

    return string.replace( pattern, replace );

}

string引数にはシェーダーコード文字列を渡すと展開されたコード文字列を返すという簡単な関数である。

でこのShaderChunkの中身を理解すれば、コードの構築が端折れそうだが、この中身をすべて理解するのは大変である。カスタマイズする部分のみに着目するのがよさそうだ。今回はattribute変数をフラグメントシェーダーに渡す部分を頂点シェーダーに書き、それを受けてテクスチャーマッピングする部分をフラグメントシェーダーに追加すればよい。
頂点シェーダーについては既存のコードはそのままおいておいて、attribute変数の追加と、それをフラグメントシェーダーに引き渡すコードだけを書けばよさそうである。

実際に書いたコードは以下である。

vertex shader:

#define PHONG

varying vec3 vViewPosition;

#ifndef FLAT_SHADED

    varying vec3 vNormal;

#endif

#include <common>
#include <uv_pars_vertex>
#include <uv2_pars_vertex>
#include <displacementmap_pars_vertex>
#include <envmap_pars_vertex>
#include <color_pars_vertex>
#include <fog_pars_vertex>
#include <morphtarget_pars_vertex>
#include <skinning_pars_vertex>
#include <shadowmap_pars_vertex>
#include <logdepthbuf_pars_vertex>
#include <clipping_planes_pars_vertex>

varying vec3 vNormalView;

// attributeの追加
attribute float texIndex;
attribute float amount;
// フラグメントシェーダーに引き渡す値の定義
varying float vTexIndex;
varying float vAmount;

void main() {

    #include <uv_vertex>
    #include <uv2_vertex>
    #include <color_vertex>

    #include <beginnormal_vertex>
    #include <morphnormal_vertex>
    #include <skinbase_vertex>
    #include <skinnormal_vertex>
    #include <defaultnormal_vertex>

#ifndef FLAT_SHADED // Normal computed with derivatives when FLAT_SHADED

    vNormal = normalize( transformedNormal );

#endif

    #include <begin_vertex>
    #include <displacementmap_vertex>
    #include <morphtarget_vertex>
    #include <skinning_vertex>
    #include <project_vertex>
    #include <logdepthbuf_vertex>
    #include <clipping_planes_vertex>

    vViewPosition = - mvPosition.xyz;

    #include <worldpos_vertex>
    #include <envmap_vertex>
    #include <shadowmap_vertex>
    #include <fog_vertex>
    // コードの追加
    vTexIndex = texIndex;
    vAmount = amount;
    vNormalView = normal;
}

fragment shader:

#define PHONG

uniform vec3 diffuse;
uniform vec3 emissive;
uniform vec3 specular;
uniform float shininess;
uniform float opacity;

#include <common>
#include <packing>
#include <dithering_pars_fragment>
#include <color_pars_fragment>
#include <uv_pars_fragment>
#include <uv2_pars_fragment>
#include <map_pars_fragment>
#include <alphamap_pars_fragment>
#include <aomap_pars_fragment>
#include <lightmap_pars_fragment>
#include <emissivemap_pars_fragment>
#include <envmap_pars_fragment>
#include <gradientmap_pars_fragment>
#include <fog_pars_fragment>
#include <bsdfs>
#include <lights_pars>
#include <lights_phong_pars_fragment>
#include <shadowmap_pars_fragment>
#include <bumpmap_pars_fragment>
#include <normalmap_pars_fragment>
#include <specularmap_pars_fragment>
#include <logdepthbuf_pars_fragment>
#include <clipping_planes_pars_fragment>

varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;

void main() {

    #include <clipping_planes_fragment>

    vec4 diffuseColor = vec4( diffuse, opacity );
    ReflectedLight reflectedLight = ReflectedLight( vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ), vec3( 0.0 ) );
    vec3 totalEmissiveRadiance = emissive;

    #include <logdepthbuf_fragment>
    // シェーダーのカスタマイズ
    //#include <map_fragment>

    //float ycomp = dot(vec3(0.,0.,1.),vNormalView);
    //テクスチャ中のセル番号
    float texIdx;
    // 法線のz成分があれば底面か上面とする
    if(vNormalView.z != 0.0){
      //上面・底面用のテクスチャのセル番号 
      texIdx = MAX_TEX_NUM - vTexIndex - 8.0 - 1.0;
    } else {
      //側面用のテクスチャのセル番号を求める
      texIdx = MAX_TEX_NUM - vTexIndex - 1.0;
    }
    vec2 uv;
    // texIdxからテクスチャ上のuv開始座標を求める
    uv.y = floor(texIdx / TEX_DIV) * TEX_DIV_R;
    uv.x = mod(texIdx,TEX_DIV ) * TEX_DIV_R ;
    vec2 vuv;
    // 上面・底面と側面とで処理を分ける。 
    if(vNormalView.z == 0.0){
      //側面用 
      vuv = vec2(vUv.x * TEX_DIV_R,mod(vUv.y , 1.0 / vAmount) * vAmount * TEX_DIV_R / 8.0);
    } else {
      //上面・底面用 
      vuv = vUv * TEX_DIV_R;
    }
    vec4 texelColor = texture2D(map, vuv + uv);

    //vec4 texelColor = texture2D( map, vUv );

    texelColor = mapTexelToLinear( texelColor );
    diffuseColor *= texelColor;

    #include <color_fragment>
    #include <alphamap_fragment>
    #include <alphatest_fragment>
    #include <specularmap_fragment>
    #include <normal_flip>
    #include <normal_fragment>
    #include <emissivemap_fragment>

    // accumulation
    #include <lights_phong_fragment>
    #include <lights_template>

    // modulation
    #include <aomap_fragment>

    vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular + totalEmissiveRadiance;

    #include <envmap_fragment>

    gl_FragColor = vec4( outgoingLight, diffuseColor.a );

    #include <tonemapping_fragment>
    #include <encodings_fragment>
    #include <fog_fragment>
    #include <premultiplied_alpha_fragment>
    #include <dithering_fragment>

}

2つのシェーダーとも、追加コードは最低限で所望の処理を実現することができている。あとはShaderMaterialにこのシェーダーを引き渡して描画するだけである。

const baseShader =  THREE.ShaderLib['phong'];
   const mat = new THREE.ShaderMaterial({
      uniforms:THREE.UniformsUtils.clone(baseShader.uniforms),
      defines: {
        MAX_TEX_NUM: toFloatString(MAX_TEX_NUM),
        TEX_DIV: toFloatString(TEX_DIV),
        TEX_DIV_R: toFloatString(TEX_DIV_R),
        USE_MAP:''
      },
      lights:true,
      vertexShader: vertexShader,
      fragmentShader: fragmentShader
    });
    mat.uniforms.emissive.value = new THREE.Color(0x000000);
    mat.uniforms.ambientLightColor.value = new THREE.Color(0x303030);
    mat.uniforms.map.value = buildingsTextures[0];


    mesh.add(new THREE.Mesh(
      new THREE.Geometry(),
      mat
    ));

とはいってもまだコードには不具合があり、その原因を探っているところである。

この話は続く。

デモ&ソースコード

動作サンプル

新しいウィンドウで開く

ソースコード・リソース

/dev/map/0005/index.html

/dev/map/0005/main.js

/dev/map/0005/readme.md

/dev/map/0005/texture.jpg

/dev/map/0005/three.js

/dev/map/0005/thumbnail.png

/dev/map/0005/tsconfig.json

/dev/map/0005/typings.json