読者です 読者をやめる 読者になる 読者になる

S.F. Page

Programming,Music,etc...

three.jsを使うのやめWebGLで書き直すことにする。

ここまではthree.jsを使って実装してきた。

実際のところthree.jsを使うと、WebGLや行列、ベクトルは意識しなくても実装できるようになっている。ただシェーダーを直にいじるとか、低レベルな実装をする場合はthree.jsの内部構造も意識して実装しなくてはならない。だが、WebGLに比べて、APIの内部構造のドキュメントは少なく、直にソースコードを覗き、解析を行う必要がある。でもその内部構造は当然のことながら、WebGLを使って実装されているから、WebGLの知識は必要なのだ。シェーダーを書くことはなくても、例えばBufferGeometryとGeometryの違いや利点は、WebGLのVertex Buffer Objectのことを知っていないと理解できない。私の結論としては「WebGLに関する知識は相当に持っておく必要がある」だ。

私の場合それに加えて、頂点周りの行列処理や、ライティングに関する知識も乏しいため、three.jsの内部について理解を深めることは困難を極める。

そういうわけでthree.jsを使うのをやめ、APIを使って再実装することにした。 対象とするAPIはWebGL 2.0としたい。といってもいきなりの実装はできないので、知識を深めながら進めることにする。教材としてはwgld.orgを使わせていただこうと思っている。ただここで書かれているサンプルコードは、若干古いので書き直しながら進めようと思っている。結局のところwgld.orgにあるサンプルコードをES6を意識しながら書き直していくことで、知識を深めようという作戦をとることにする。

そういうわけで、まず最初に実装したのが以下である。

https://bl.ocks.org/sfpgmr/47c33ee5ac5199e126fa6a6b2f974f80

三角形ポリゴンを一枚描くという、APIのチュートリアルではほぼ定番のコードである。wgld.orgではポリゴンに色を塗る(頂点色の指定) に相当する。

ちょっとヘンテコ改造になっているけどね。。例えばminMatrix.jsなどは以下の通り。

// ------------------------------------------------------------------------------------------------
// minMatrix.js
// version 0.0.1
// Copyright (c) doxas
// 修正:sfpgmr
// ------------------------------------------------------------------------------------------------

class Mat4 {
  constructor(m = new Float32Array(16)) {
    this.matrix = m;
  }

  identity() {
    const dest = this.matrix;
    dest[0] = 1; dest[1] = 0; dest[2] = 0; dest[3] = 0;
    dest[4] = 0; dest[5] = 1; dest[6] = 0; dest[7] = 0;
    dest[8] = 0; dest[9] = 0; dest[10] = 1; dest[11] = 0;
    dest[12] = 0; dest[13] = 0; dest[14] = 0; dest[15] = 1;
  }

  multiply(matrix, destMatrix = new Mat4()) {
    const mat1 = this.matrix, mat2 = matrix.matrix;
    const dest = destMatrix.matrix;
    const a = mat1[0], b = mat1[1], c = mat1[2], d = mat1[3],
      e = mat1[4], f = mat1[5], g = mat1[6], h = mat1[7],
      i = mat1[8], j = mat1[9], k = mat1[10], l = mat1[11],
      m = mat1[12], n = mat1[13], o = mat1[14], p = mat1[15],

      A = mat2[0], B = mat2[1], C = mat2[2], D = mat2[3],
      E = mat2[4], F = mat2[5], G = mat2[6], H = mat2[7],
      I = mat2[8], J = mat2[9], K = mat2[10], L = mat2[11],
      M = mat2[12], N = mat2[13], O = mat2[14], P = mat2[15];

    dest[0] = A * a + B * e + C * i + D * m;
    dest[1] = A * b + B * f + C * j + D * n;
    dest[2] = A * c + B * g + C * k + D * o;
    dest[3] = A * d + B * h + C * l + D * p;
    dest[4] = E * a + F * e + G * i + H * m;
    dest[5] = E * b + F * f + G * j + H * n;
    dest[6] = E * c + F * g + G * k + H * o;
    dest[7] = E * d + F * h + G * l + H * p;
    dest[8] = I * a + J * e + K * i + L * m;
    dest[9] = I * b + J * f + K * j + L * n;
    dest[10] = I * c + J * g + K * k + L * o;
    dest[11] = I * d + J * h + K * l + L * p;
    dest[12] = M * a + N * e + O * i + P * m;
    dest[13] = M * b + N * f + O * j + P * n;
    dest[14] = M * c + N * g + O * k + P * o;
    dest[15] = M * d + N * h + O * l + P * p;
    return destMatrix;
  };

  scale(vec, destMatrix = new Mat4()) {
    const mat = this.matrix;
    const dest = destMatrix.matrix;

    dest[0] = mat[0] * vec[0];
    dest[1] = mat[1] * vec[0];
    dest[2] = mat[2] * vec[0];
    dest[3] = mat[3] * vec[0];
    dest[4] = mat[4] * vec[1];
    dest[5] = mat[5] * vec[1];
    dest[6] = mat[6] * vec[1];
    dest[7] = mat[7] * vec[1];
    dest[8] = mat[8] * vec[2];
    dest[9] = mat[9] * vec[2];
    dest[10] = mat[10] * vec[2];
    dest[11] = mat[11] * vec[2];
    dest[12] = mat[12];
    dest[13] = mat[13];
    dest[14] = mat[14];
    dest[15] = mat[15];
    return destMatrix;
  };

  translate(vec, destMatrix = new Mat4()) {
    const mat = this.matrix;
    const dest = destMatrix.matrix;

    dest[0] = mat[0]; dest[1] = mat[1]; dest[2] = mat[2]; dest[3] = mat[3];
    dest[4] = mat[4]; dest[5] = mat[5]; dest[6] = mat[6]; dest[7] = mat[7];
    dest[8] = mat[8]; dest[9] = mat[9]; dest[10] = mat[10]; dest[11] = mat[11];
    dest[12] = mat[0] * vec[0] + mat[4] * vec[1] + mat[8] * vec[2] + mat[12];
    dest[13] = mat[1] * vec[0] + mat[5] * vec[1] + mat[9] * vec[2] + mat[13];
    dest[14] = mat[2] * vec[0] + mat[6] * vec[1] + mat[10] * vec[2] + mat[14];
    dest[15] = mat[3] * vec[0] + mat[7] * vec[1] + mat[11] * vec[2] + mat[15];
    return destMatrix;
  };

  rotate(angle, axis, destMatrix = new Mat4()) {
    const dest = destMatrix.matrix;
    const mat = this.matrix;
    var sq = Math.sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]);
    if (!sq) { return null; }
    var a = axis[0], b = axis[1], c = axis[2];
    if (sq != 1) { sq = 1 / sq; a *= sq; b *= sq; c *= sq; }
    var d = Math.sin(angle), e = Math.cos(angle), f = 1 - e,
      g = mat[0], h = mat[1], i = mat[2], j = mat[3],
      k = mat[4], l = mat[5], m = mat[6], n = mat[7],
      o = mat[8], p = mat[9], q = mat[10], r = mat[11],
      s = a * a * f + e,
      t = b * a * f + c * d,
      u = c * a * f - b * d,
      v = a * b * f - c * d,
      w = b * b * f + e,
      x = c * b * f + a * d,
      y = a * c * f + b * d,
      z = b * c * f - a * d,
      A = c * c * f + e;
    if (angle) {
      if (mat != dest) {
        dest[12] = mat[12]; dest[13] = mat[13];
        dest[14] = mat[14]; dest[15] = mat[15];
      }
    } else {
      dest = mat;
    }
    dest[0] = g * s + k * t + o * u;
    dest[1] = h * s + l * t + p * u;
    dest[2] = i * s + m * t + q * u;
    dest[3] = j * s + n * t + r * u;
    dest[4] = g * v + k * w + o * x;
    dest[5] = h * v + l * w + p * x;
    dest[6] = i * v + m * w + q * x;
    dest[7] = j * v + n * w + r * x;
    dest[8] = g * y + k * z + o * A;
    dest[9] = h * y + l * z + p * A;
    dest[10] = i * y + m * z + q * A;
    dest[11] = j * y + n * z + r * A;
    return destMatrix;
  };

  static createLookAt(eye, center, up, destMatrix = new Mat4()) {
    const dest = destMatrix.matrix;
    const eyeX = eye[0], eyeY = eye[1], eyeZ = eye[2],
      upX = up[0], upY = up[1], upZ = up[2],
      centerX = center[0], centerY = center[1], centerZ = center[2];
    if (eyeX == centerX && eyeY == centerY && eyeZ == centerZ) { return dest.identity(); }
    var x0, x1, x2, y0, y1, y2, z0, z1, z2, l;
    z0 = eyeX - center[0];
    z1 = eyeY - center[1];
    z2 = eyeZ - center[2];
    l = 1 / Math.sqrt(z0 * z0 + z1 * z1 + z2 * z2);
    z0 *= l; z1 *= l; z2 *= l;
    x0 = upY * z2 - upZ * z1;
    x1 = upZ * z0 - upX * z2;
    x2 = upX * z1 - upY * z0;
    l = Math.sqrt(x0 * x0 + x1 * x1 + x2 * x2);
    if (!l) {
      x0 = 0; x1 = 0; x2 = 0;
    } else {
      l = 1 / l;
      x0 *= l; x1 *= l; x2 *= l;
    }
    y0 = z1 * x2 - z2 * x1; y1 = z2 * x0 - z0 * x2; y2 = z0 * x1 - z1 * x0;
    l = Math.sqrt(y0 * y0 + y1 * y1 + y2 * y2);
    if (!l) {
      y0 = 0; y1 = 0; y2 = 0;
    } else {
      l = 1 / l;
      y0 *= l; y1 *= l; y2 *= l;
    }
    dest[0] = x0; dest[1] = y0; dest[2] = z0; dest[3] = 0;
    dest[4] = x1; dest[5] = y1; dest[6] = z1; dest[7] = 0;
    dest[8] = x2; dest[9] = y2; dest[10] = z2; dest[11] = 0;
    dest[12] = -(x0 * eyeX + x1 * eyeY + x2 * eyeZ);
    dest[13] = -(y0 * eyeX + y1 * eyeY + y2 * eyeZ);
    dest[14] = -(z0 * eyeX + z1 * eyeY + z2 * eyeZ);
    dest[15] = 1;
    return destMatrix;
  };

  static createPerspective(fovy, aspect, near, far, destMatrix = new Mat4()) {
    const dest = destMatrix.matrix;
    const t = near * Math.tan(fovy * Math.PI / 360);
    const r = t * aspect;
    const a = r * 2, b = t * 2, c = far - near;
    dest[0] = near * 2 / a;
    dest[1] = 0;
    dest[2] = 0;
    dest[3] = 0;
    dest[4] = 0;
    dest[5] = near * 2 / b;
    dest[6] = 0;
    dest[7] = 0;
    dest[8] = 0;
    dest[9] = 0;
    dest[10] = -(far + near) / c;
    dest[11] = -1;
    dest[12] = 0;
    dest[13] = 0;
    dest[14] = -(far * near * 2) / c;
    dest[15] = 0;
    return destMatrix;
  };

  transpose(destMatrix = new Mat4()) {
    const mat = this.matrix;
    const dest = destMatrix.matrix;
    dest[0] = mat[0]; dest[1] = mat[4];
    dest[2] = mat[8]; dest[3] = mat[12];
    dest[4] = mat[1]; dest[5] = mat[5];
    dest[6] = mat[9]; dest[7] = mat[13];
    dest[8] = mat[2]; dest[9] = mat[6];
    dest[10] = mat[10]; dest[11] = mat[14];
    dest[12] = mat[3]; dest[13] = mat[7];
    dest[14] = mat[11]; dest[15] = mat[15];
    return destMatrix;
  };

  inverse(destMatrix = new Mat4()) {
    const dest = destMatrix.matrix;
    const mat = this.matrix;
    const a = mat[0], b = mat[1], c = mat[2], d = mat[3],
      e = mat[4], f = mat[5], g = mat[6], h = mat[7],
      i = mat[8], j = mat[9], k = mat[10], l = mat[11],
      m = mat[12], n = mat[13], o = mat[14], p = mat[15],
      q = a * f - b * e, r = a * g - c * e,
      s = a * h - d * e, t = b * g - c * f,
      u = b * h - d * f, v = c * h - d * g,
      w = i * n - j * m, x = i * o - k * m,
      y = i * p - l * m, z = j * o - k * n,
      A = j * p - l * n, B = k * p - l * o,
      ivd = 1 / (q * B - r * A + s * z + t * y - u * x + v * w);
    dest[0] = (f * B - g * A + h * z) * ivd;
    dest[1] = (-b * B + c * A - d * z) * ivd;
    dest[2] = (n * v - o * u + p * t) * ivd;
    dest[3] = (-j * v + k * u - l * t) * ivd;
    dest[4] = (-e * B + g * y - h * x) * ivd;
    dest[5] = (a * B - c * y + d * x) * ivd;
    dest[6] = (-m * v + o * s - p * r) * ivd;
    dest[7] = (i * v - k * s + l * r) * ivd;
    dest[8] = (e * A - f * y + h * w) * ivd;
    dest[9] = (-a * A + b * y - d * w) * ivd;
    dest[10] = (m * u - n * s + p * q) * ivd;
    dest[11] = (-i * u + j * s - l * q) * ivd;
    dest[12] = (-e * z + f * x - g * w) * ivd;
    dest[13] = (a * z - b * x + c * w) * ivd;
    dest[14] = (-m * t + n * r - o * q) * ivd;
    dest[15] = (i * t - j * r + k * q) * ivd;
    return destMatrix;
  };
}

あと元コードはWebGL 1.0ベースであるが、それを WebGL 2.0ベースに書き直している。シェーダーコードはHTMLに書くのではなく、jsコード中にテンプレート文字列を使って書いている。es6の現代にあっては、シェーダーコードはjsファイル中に書くべきだと私は思っている。

実をいうとここまでのレベルのコードは、DirectX + C++でコードを書いていた時代でも書けていた。たしかDirectX 11あたりまでは追っかけていたと思う。シェーダーコードについてもGLSLではなくHLSLだが書いていた。

ただ過去記事を振り返ると、2D描画が目的なので、3Dへの取り組みは少し触れる程度にとどまっている。
3Dは2Dよりずっと難しいからだ。ただ当時にくらべてwgld.orgなどの良コンテンツもあるので、以前よりはハードルは下がっているので、じっくり時間をかけて勉強しようと思う。と言いつつも途中で飽きる可能性は高いけど。。

もう当初のシューティングゲームを作るという目的からは外れているが、趣味なのでよしとしよう。。

THREE.ShaderMaterialによる改良版が一応完成に至る。- Overpass APIとthree.jsで地図を3D表示(7)

バグの発生

下の動画を見ていただくとわかると思うが、上面のテクスチャマップがうまくいかない問題が発生した。

この問題はテクスチャ番号として「3」を指定した時に発生する。テクスチャ番号0-2ではなぜか発生しない。

今一度、今回の取り組みを解説しておくと

テクスチャマッピングはShaderMaterialを使ってやや特殊な方法で行っている。 頂点情報のattributeとして、テクスチャ・インデックス(texIndex)、フロア階数(amount)を持たせ、それをフラグメントシェーダーに渡すことで、フロア階数に応じ、壁面は1フロア分のテクスチャを階数分繰り返すようにマッピングし、上面と底面は普通にテクスチャ・マッピングしているのである。

テクスチャ・ビットマップは下のように4×4セルのビットマップとなっている。大きさは2048x2048pixelである。ビルの壁面用としては00-03、上面・底面用のテクスチャとしては08-11を使用する。

コードの実装内容は以下の通りである。

1.ExtrudeBufferGeometryで作った建物geometryオブジェクトにtexIndex、amountのattributeを追加する。

      const texIndexs = new Uint16Array(geometry.attributes.position.count);
      const amounts = new Uint16Array(geometry.attributes.position.count);
      geometry.addAttribute('texIndex', new THREE.BufferAttribute(texIndexs, 1,false));
      geometry.addAttribute('amount', new THREE.BufferAttribute(amounts, 1,false));

2.追加したtexIndexに建物のテクスチャ・インデックス00-03をランダムにセット、amountには建物のフロア階数をセットする。

      for (let i = 0, e = geometry.attributes.position.count; i < e; ++i) {
        texIndexs[i] = data.texNo;
        amounts[i] = data.amount;
      }

3.頂点シェーダーでは建物のテクスチャ・インデックスと建物のフロア階数をフラグメント・シェーダーに引き渡す。

.
.
.
// テクスチャ・インデックス
attribute float texIndex;
// フロア階数情報
attribute float amount;
varying float vTexIndex;
varying float vAmount;
varying vec3 vNormalView;
.
.
.
void main(){
.
.
.
  // テクスチャ番号をフラグメントシェーダーに引き渡す
  vTexIndex = texIndex;
  // フロア階数情報をフラグメントシェーダーに引き渡す
  vAmount = amount;
  // 法線ベクトルをフラグメントシェーダーに引き渡す
  vNormalView = normal;
}

4.フラグメントシェーダーでは引き渡された建物のテクスチャ・インデックス(texIndex)とフロア階数(amount)をもとにテクスチャを読み出すためのuv座標を求め、テクスチャを読み出す。

.
.
.

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

void main() {
.
.
.
 // 
  float texIdx;
  if(vNormalView.z != 0.0){// 法線ベクトルのz成分があれば上面とみなす
    // 上面用のテクスチャ・インデックスを計算で求める
    texIdx = MAX_TEX_NUM - vTexIndex - 8.0 - 1.0;
  } else {
    // 壁面用のインデックスを求める
    texIdx = MAX_TEX_NUM - vTexIndex - 1.0;
  }

  //テクスチャ・インデックスからマッピング開始位置のuv座標を求める
  vec2 uv;
  uv.y = floor(texIdx / (TEX_DIV)) * TEX_DIV_R;
  uv.x = mod(texIdx,TEX_DIV ) * TEX_DIV_R ;
  // 開始位置からuv量を求める。
  vec2 vuv;
  if(vNormalView.z == 0.0){
    // 壁面の場合
    vuv = vec2(vUv.x * TEX_DIV_R,mod(vUv.y,1.0 / vAmount)* vAmount * TEX_DIV_R * 0.125/* 1つのセルは8フロア分ありそのうちの1フロア分のみを使う*/);
  } else {
    // 上面の場合
    vuv = vUv * TEX_DIV_R;
 }
  vec4 texelColor = texture2D(map, vuv + uv);

.
.
.
}

実装したものは以下にアップしてある。テクスチャ・インデックスを3に指定すると現象が再現できる。 https://bl.ocks.org/sfpgmr/e1917c9f13455b4af39226c8835094ca

原因

原因は頂点シェーダーからフラグメント・シェーダーに値を渡すときに値が補完されるためであった。
私としては値を補完せずそのまま渡してほしいが、WebGL 1.0ではできないらしい。
(WebGL 2.0ではattributeに相当するinflatsmoothを指定できるので、これで回避できそうな気もしないでもないが。。)

とりあえずは補完を打ち消すコードをフラグメント・シェーダーに入れることで解消することができた。
具体的には四捨五入するだけである。

float t = floor(vTexIndex + .5);

修正したコードは以下である。以下のコードは00-03以外のテクスチャ・インデックスを指定した場合は普通にテクスチャ・マッピングするように改良している。

https://bl.ocks.org/sfpgmr/61fe805bb2a72bda86eff955838fda94

地図の描画コードに反映させる

これを機に、GeometryからBufferGeometryに変更した。
実はここでも問題が発生した。geometryをまとめるのにBufferGeomtry.mergeを使おうとしたが、r85バージョンではこのメソッドには不具合がある。 ソースコードは以下のとおりである。

merge: function ( geometry, offset ) {

        if ( ( geometry && geometry.isBufferGeometry ) === false ) {

            console.error( 'THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry );
            return;

        }

        if ( offset === undefined ) offset = 0;

        var attributes = this.attributes;

        for ( var key in attributes ) {

            if ( geometry.attributes[ key ] === undefined ) continue;

            var attribute1 = attributes[ key ];
            var attributeArray1 = attribute1.array;

            var attribute2 = geometry.attributes[ key ];
            var attributeArray2 = attribute2.array;

            var attributeSize = attribute2.itemSize;

            for ( var i = 0, j = attributeSize * offset; i < attributeArray2.length; i ++, j ++ ) {

                attributeArray1[ j ] = attributeArray2[ i ];

            }

        }

        return this;

    },

問題点は以下の2点である。

  1. indexを統合するコードがない。
  2. attributeの統合コードが、TypedArrayを考慮したものになっていない。

この問題点はthree.jsのIssueに載っている。
https://github.com/mrdoob/three.js/issues/6188

具体的に修正するコード例も上のIssueに載っていたが、実はこの修正例にも不具合がある。それをさらに修正したバージョンが以下である。

/***
 * @param {Float32Array} first
 * @param {Float32Array} second
 * @returns {Float32Array}
 * @constructor
 */
function Float32ArrayConcat(first, second) {
  var firstLength = first.length,
    result = new Float32Array(firstLength + second.length);

  result.set(first);
  result.set(second, firstLength);

  return result;
}

/**
 * @param {Uint32Array} first
 * @param {Uint32Array} second
 * @returns {Uint32Array}
 * @constructor
 */
function Uint32ArrayConcat(first, second) {
  var firstLength = first.length,
    result = new Uint32Array(firstLength + second.length);

  result.set(first);
  result.set(second, firstLength);

  return result;
}

THREE.BufferGeometry.prototype.merge = function (geometry) {

  if (geometry instanceof THREE.BufferGeometry === false) {

    console.error('THREE.BufferGeometry.merge(): geometry not an instance of THREE.BufferGeometry.', geometry);
    return;

  }

  var attributes = this.attributes;

  if (this.index) {

    var indices = geometry.index.array;

    var offset = this.index.array.length;

    for (var i = 0, il = indices.length; i < il; i++) {

      indices[i] = offset + indices[i];

    }

    this.setIndex(new THREE.BufferAttribute(Uint32ArrayConcat(this.index.array, indices),1));

  }

  for (var key in attributes) {

    if (geometry.attributes[key] === undefined) continue;

    const dest = attributes[key].array;
    const src = geometry.attributes[key].array;

    attributes[key].array = Float32ArrayConcat(attributes[key].array, geometry.attributes[key].array);
    attributes[key].count = attributes[key].array.length / attributes[key].itemSize;


  }
  return this;
};

ただこのメソッド、頂点数が多いととてつもなく遅くなる。なので統合するコードは別に書くことにした。

最終的な結果が以下である。

https://bl.ocks.org/sfpgmr/2eba19ac24b355c12830b3acf26600d8

改良前の画像は以下。

改良後の画像は以下である。フロア階数を反映した壁面となっているのがお分かりいただけるかと思う。

一応完成したが、もういくつか別のやり方がありそうで、ちょっとそれを試してみようと思っている。

頂点数をカウントしたら5,682,312あった。すごいね。。

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

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

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

  

デモ&ソースコード
ExtrudeGeometry&TextureMapping - bl.ocks.org

階数とテクスチャー番号のアトリビュートを用意して、それを元にフラグメントシェーダーで階数に応じてうまくテクスチャを貼り付けるようにした。ただテクスチャを貼り付けるところ以外は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
    ));

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

この話は続く。