WebGL 2.0 で追加された3Dテクスチャ(TEXTURE_3D)を使うと、3次元のテクスチャ画像を用いて空間的な表現が可能になります。これにより、形状を頂点で定義するトライアングルベースとは違ったモデル表現や、3次元的に連続したノイズ表現が可能になります。

一般的なテクスチャはポリゴンに対して適用する

3D表現において馴染み深いテクスチャですが、テクスチャと聞いて一般的にイメージするのはポリゴンに対して適用する画像データでしょう。テクスチャを貼る対象が壁のような平面であれば壁紙を貼るように適用します。球体であれば球を紙で包み込むようにし、球を構成する1つ1つのポリゴン(WebGLでは三角形)に合わせて適用します。いずれにせよ、形状を構成するポリゴンに対して適用することになります。

これはつまり、形状の「面」に対してのみ画像データが適用されることになります。ポリゴンで構成された3Dモデルの中身は空洞のため、モデルを切断したり視点を移動させてモデルの中を覗くと、中身にまではテクスチャが反映されていないことがわかります。

3Dテクスチャは空間に対して適用する

3Dテクスチャは3次元空間内のそれぞれの点における画像データを定義できます。平面のテクスチャ画像を上に積み重ねていくことで空間を表現するイメージです。この3Dテクスチャをモデルに対して適用すると、ポリゴンの表面だけでなく、モデル内の空間にも対応した画像データが存在するため、モデルを切断しても中身の詰まったような表現が可能になります

モデルを切断したり、半透明にして中身の情報を表示したい場合にボリュームデータを扱った3Dテクスチャは有効な手段です。また、直接的な表示画像だけでなく、ノイズの情報や場のパラメーターを3Dテクスチャで扱うことで、普通の3Dモデルとはひとあじ違った表現が可能になります。

サンプルの紹介

3Dテクスチャ(TEXTURE_3D)を使ったサンプルを紹介します。

このサンプルでは、CTスキャンで撮影した人体の上半身を3Dテクスチャを使用してボリュームレンダリングで表示しています。もとの撮影画像はこちらのように、人体を縦に輪切りした連続の白黒画像です。

*本サンプルのCT画像は、筆者が検診の際に撮影したものを画像データとしていただいたものです。病院によっては、撮影時にお願いすれば記録メディア代の実費のみで画像データを持ち帰りできるところもあるので、機会があればダメ元で言ってみましょう。筆者は次は脳の画像データを狙っています。

サンプルの右上部UIで下記のパラメーターを調整できます。

  • iteration: 後述のサンプリング繰り返し回数です。この設定値が大きいほど表示が精細になりますが、負荷が大きくなるので注意してください
  • baseAlpha: 透明度です
  • threshold: 表示する濃淡色のしきい値です。CT画像は撮影対象の密度(*)が濃淡で表されており、白を1、黒を0として、この設定値未満の色を表示から除外します。設定値を大きくすることで密度の濃い骨のみの表示などができます。また、画像の不要な部分(周りの黒や濃い灰色の非人体部分)を取り除くためにも使用しています
  • slicePosition: 輪切りにした人体の下からの表示開始位置です。内部をよく観察したい場合に 透明度やしきい値をオフにした状態でこのスライダーを動かすことで、立方体が金太郎飴のように中身がつまったデータであることがわかります

*実際にはCT値といって、X線の吸収具合を表しているそうです

サンプルの仕組みについて簡単に説明します。

  1. 立方体のモデルを定義し、レンダリングを行う
  2. 頂点シェーダーでは通常と同じように立方体の頂点を処理する
  3. フラグメントシェーダーでは、まず処理しているターゲットのピクセル(点S)の3D空間内の座標(点P0)を計算する
  4. 前ステップで求めた座標をもとに、視点(点O)からターゲットのピクセルへ向かう単位ベクトル(レイRi)を計算する
  5. 3D空間内の座標を3Dテクスチャの座標(uvwの各成分が0から1の間の値)にみたて、サンプリングを行って点P0のテクセルカラーを取得する
  6. 注目する座標をレイの方向に進め(点P1)、その座標から再度対応した3Dテクスチャのテクセルカラーを取得し、色を足し合わせる
  7. レイを進め、3Dテクスチャからカラーを取得して足し合わせる処理を繰り返し回数分行う(点P2、点P3…)
  8. この足し合わせた色が視点から見たピクセルの色となる。フラグメントシェーダーはスクリーン上のすべてのピクセルについてこの色計算を行う

APIの使い方

3Dテクスチャの使い方は、実はテクスチャ配列(TEXTURE_2D_ARRAY)とほとんど同じです。テクスチャ配列では各APIのターゲットにTEXTURE_2D_ARRAYを指定していましたが、3DテクスチャではTEXTURE_3Dを指定します。TEXTURE_2D_ARRAYの記事についても参照ください。

3DテクスチャはWebGL 2.0 の機能なので、最初にWebGL 2.0 のコンテキストを取得します。以下gl2は、ここで取得したWebGL2RenderingContextオブジェクトを指します。

// WebGL2コンテキスト(WebGL2RenderingContext)を取得
const gl2 = canvas.getContext('webgl2');

テクスチャ配列同様、3Dテクスチャの使用手順の大部分は通常の2Dテクスチャと変わりありません。手順2で触れますが、「APIのターゲットにTEXTURE_2Dの代わりにTEXTURE_3Dを指定する以外はまったく同じ」という手順もあります。そういった手順の場合タイトルに「(同)」をつけていますので、通常のテクスチャでの手順との違いの目安にしてください。

1. テクスチャを作成する(同)

テクスチャを作成します。こちらは3Dテクスチャでも通常のテクスチャでも一緒です。

// テクスチャを作成
const texture = gl2.createTexture();

2. テクスチャをバインドする(同)

テクスチャに画像データを転送する前に、テクスチャをバインドします。「WebGLのオブジェクトを操作する場合は事前にバインド」というWebGLのルールは WebGL 2.0 でも変わりません。通常のテクスチャはbindTexture()メソッドの第一引数(バインドのターゲット)としてTEXTURE_2Dを使用していましたが、3DテクスチャではTEXTURE_3Dを使用します。テクスチャをバインドする前にactiveTexture()メソッドを呼んで明示的に0番のテクスチャユニットを指定しています。

// 0番のテクスチャユニットを指定
gl2.activeTexture(gl2.TEXTURE0);

// バインドするターゲットとしてTEXTURE_3Dを指定
gl2.bindTexture(gl2.TEXTURE_3D, texture);

3. テクスチャに画像データを転送する

次に、テクスチャに画像データを転送します。通常の2Dテクスチャでは画像データの転送にtexImage2D()メソッドを使用しましたが、3DテクスチャではtexImage3D()メソッドを使用します。texImage3D()メソッドは3次元のテクスチャを転送するためのメソッドです。テクスチャ配列(TEXTURE_2D_ARRAY)同様、3Dテクスチャは3次元となるので、このメソッドを使用します。

texImage3D()メソッドには、下記の引数を指定します。

引数名 内容
第一引数 target ターゲット(3Dテクスチャを使用する場合は、gl2.TEXTURE_3D
第二引数 level ミップマップレベル(手動で各ミップマップレベルを転送する場合以外は0を指定)
第三引数 internalFormat テクスチャのフォーマット
第四引数 width テクスチャの幅
第五引数 height テクスチャの高さ
第六引数 depth テクスチャの深さ(3Dテクスチャにおいては深さ方向のテクスチャ枚数)
第七引数 border ボーダーの幅(WebGLでは固定で0を指定)
第八引数 format テクスチャソースのフォーマット
第九引数 type テクスチャソースのデータタイプ
第十引数 source テクスチャソース

引数が多くて複雑なので、使い慣れていないうちは下記の例を見て幅や高さを変更して使いましょう。

// 転送にはtexImage3D()メソッドを使用する
gl2.texImage3D(
  gl2.TEXTURE_3D, // 3Dテクスチャを使用するため固定
  0,                    // ミップマップは後でgenerateMipmap()で作成するため、0固定
  gl2.RGBA,             // テクスチャで使用するフォーマット
  512,                  // 幅512 × 高さ512のテクスチャ
  512,                  // 幅512 × 高さ512のテクスチャ
  50,                   // 深さ方向に50枚の3Dテクスチャ
  0,                    // WebGLでは0固定
  gl2.RGBA,             // ピクセルデータのフォーマット
  gl2.UNSIGNED_BYTE,    // Uint8Arrayのピクセルデータを使用するため、UNSIGNED_BYTEを指定する
  pixelData             // ピクセルデータ
);

3Dテクスチャ内のすべてのテクスチャは同じサイズ(幅、高さ)にする必要があります。異なるサイズのテクスチャを混ぜて3Dテクスチャとして管理することはできません。

第十引数のsourceにテクスチャで使用する画像を指定しますが、ここに少し注意点があります。指定する画像データとしてtexImage2D()メソッドと同じく、Imageオブジェクト(HTMLImageElement)やキャンバス(HTMLCanvasElement)を指定できますが、このとき、3Dテクスチャ内の複数の画像データが縦に並んでいる必要があります。上記の画像サイズの例だと、画像の高さ1〜512pxまでが1枚目の画像、513〜1024pxまでが2枚目の画像……というように、縦に50枚の画像を並べ、都合、幅512px × 高さ32768pxの大きさの画像データを入力として指定します。

この「縦に並べてひとつにした画像データ」は、あらかじめ1枚の大きな画像としてオフラインで用意してサーバーに設置しておいてもいいですし、ランタイムで作成しても問題ありません。

今回のサンプルではsourceとして、ピクセルの画素データ配列(Uint8Array)を指定する形にしました。画像を1枚ずつdrawImage()メソッドでキャンバスに描画し、getImageData()メソッドでキャンバスから取得した画素データをつなげてひとつの大きな画素データ配列(Uint8Array)にしています。大きなキャンバスにしてもよかったのですが、キャンバスのサイズにはブラウザごとの制限があるので、キャンバスのサイズは画像1枚のサイズに固定し、取得した画素データをつなげるようにしました。

// テクスチャ1枚の要素数
// 幅 * 高さ * 4要素(RGBA)
const elementsPerTexture = IMAGE_WIDTH * IMAGE_HEIGHT * 4;

// 3Dテクスチャ用のUint8Arrayを確保
// 要素数: テクスチャ1枚の要素数 * テクスチャ数
const pixelData = new Uint8Array(elementsPerTexture * NUM_TEXTURES);

for (let i = 0; i < NUM_TEXTURES; i++) {
  // テクスチャ画像の読み込み
  // loadImage()は指定したURLの画像をImageに読み込み、ロードが終わると完了するPromiseを返却するユーザー定義関数
  const textureImage = await loadImage(`assets/${i}.jpg`);
  // テクスチャ画像をキャンバスに描画
  context2d.drawImage(textureImage, 0, 0);
  // RGBA値の取得
  const imageData = context2d.getImageData(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT);
  // RGBA値のセット
  // i番目のテクスチャなので、テクスチャi枚の要素数をオフセットに指定
  pixelData.set(imageData.data, elementsPerTexture * i);
}

4. テクスチャパラメーターを設定する(同)

フィルタリング、ラッピングなどのテクスチャパラメーターを設定します。ここでもターゲットがTEXTURE_3Dであることを除けば基本的には通常のテクスチャと使用方法は変わりません。

ただし、3Dテクスチャでは一点、2D系のテクスチャと異なる点があります。TEXTURE_2DTEXTURE_2D_ARRAYなどの2次元のテクスチャの場合は、ラッピングの設定(正規化されたテクスチャ座標0〜1の範囲外を指定した場合にどう扱うか)としてTEXTURE_WRAP_S(横方向)およびTEXTURE_WRAP_T(高さ方向)を指定していましたが、3Dテクスチャではこれらに加えてTEXTURE_WRAP_R(深さ方向)の設定も指定できます

// テクスチャパラメータを設定
gl2.texParameteri(gl2.TEXTURE_3D, gl2.TEXTURE_MAG_FILTER, gl2.LINEAR);
gl2.texParameteri(gl2.TEXTURE_3D, gl2.TEXTURE_MIN_FILTER, gl2.LINEAR_MIPMAP_LINEAR);
gl2.texParameteri(gl2.TEXTURE_3D, gl2.TEXTURE_WRAP_S, gl2.CLAMP_TO_EDGE);
gl2.texParameteri(gl2.TEXTURE_3D, gl2.TEXTURE_WRAP_T, gl2.CLAMP_TO_EDGE);
gl2.texParameteri(gl2.TEXTURE_3D, gl2.TEXTURE_WRAP_R, gl2.CLAMP_TO_EDGE);

5. ミップマップを作成する(同)

TEXTURE_MIN_FILTERテクスチャパラメーターにミップマップを使用する設定をした場合は、ミップマップの作成が必要となります。WebGLでは、バインドされているテクスチャのミップマップを自動で作成してくれる便利な機能generateMipmap()メソッドがあるのでこれを使用します。手動で作成する場合はtexImage3D()メソッドの第二引数にレベルを指定して呼び出します。ミップマップに関しても通常のテクスチャでの使用方法となんら変わりはありません(もちろんターゲットはTEXTURE_3Dです)。

// ミップマップを作成
gl2.generateMipmap(gl2.TEXTURE_3D);

6. シェーダーで3Dテクスチャをサンプリングする

シェーダーで3Dテクスチャをサンプリングするコードは下記のとおりです。まず、WebGL 2.0 のお約束、シェーダーの1行目に#version 300 esバージョンディレクティブを指定します。

#version 300 es
precision mediump float;

// sampler3Dの精度を指定
precision mediump sampler3D;

in vec2 vUv;
in float vTextureDepth;
out vec4 outColor;

// sampler2D型の代わりにsampler3D型を使用
uniform sampler3D texture3D;

void main(void)
{
  // texture()関数の第一引数にsampler3Dを指定し、第二引数に3次元のテクスチャ座標vec3(u, v, w)を指定する
  outColor = texture(texture3D, vec3(vUv, vTextureDepth));
}

テクスチャサンプラーのuniformの型は、通常の2Dテクスチャではsampler2Dを使用していましたが、3Dテクスチャではsampler3Dを使用します。sampler3D型はデフォルトでは精度の指定がされていませんので、使用する場合は精度の指定が必要です。下記にsampler3Dの宣言部分を抜き出しました。

// sampler3Dの精度を指定
precision mediump sampler3D;

// sampler2D型の代わりにsampler3D型を使用
uniform sampler3D texture3D;

WebGL 1.0 のシェーダー(GLSL ES 1.0) ではテクスチャのサンプリングにtexture2D()関数を使用していましたが、WebGL 2.0 のシェーダー(GLSL ES 3.0) ではテクスチャのサンプリングにtexture()関数を使用します。このtexture()関数の引数は、samplerの型によって下記のように異なります。

2Dテクスチャ テクスチャ配列 3Dテクスチャ
第一引数 sampler2D sampler2DArray sampler3D
第二引数 vec2(u, v) vec3(u, v, layer) vec3(u, v, w)

3Dテクスチャの場合、第二引数にはテクスチャの3次元座標を指定します。テクスチャ配列では3番目のvec3.zに整数値のレイヤー番号(何枚目のテクスチャか)を指定していましたが、3DテクスチャではUV座標と同じく、0〜1に正規化された座標値を指定します。

たとえばvec3(0.4, 0.6, 0.5)を指定した場合は配列の3Dテクスチャのuvw = (0.4, 0.6, 0.5)の位置のテクセルを取得します。テクスチャ配列と異なり、3Dテクスチャではwにdepth方向の2枚のテクスチャの間の値が指定された場合、フィルターの設定値によっては補完してブレンドされた値を取得します。

7. JavaScriptからuniformをセットする(同)

シェーダーで使用するsampler3D型のuniformに対応したロケーションをgetUniformLocation()メソッドで取得します。手順2で0番のテクスチャユニットを指定したので、このuniformロケーションにuniform1i()メソッドで0を割り当てます。こちらも通常のテクスチャと手順が変わりありません。

// テクスチャのuniform locationをシェーダーから取得する
const textureLocation = gl2.getUniformLocation(program, 'texture3D');

// 3Dテクスチャのuniform locationに0番のテクスチャユニットを指定
gl2.uniform1i(textureLocation, 0);

以上で3Dテクスチャをサンプリングしてレンダリングできます。通常のテクスチャと比較して大きく異なるのは以下の3点です。

  • TEXTURE_2Dの代わりにTEXTURE_3Dを使うこと
  • 画像データの転送にtexImage3D()メソッドを使うこと
  • シェーダーでのサンプラーの型が違うことと、texture()関数の第二引数がvec3になること

TIPS

検索キーワードに注意

3Dテクスチャを使用したテクニックについてもっと情報を知りたいとき、検索キーワードとして「3Dテクスチャ」、「3D テクスチャ」、「3D texture」などを指定しても、結果は一般的な3Dやテクスチャに関する情報が大部分を占めます。「テクスチャ」は3D表現になじみ深い単語なので、広義の「テクスチャ」が検索に引っかかってしまうためです。

そういった場合は、「TEXTURE_3D」(OpenGL/WebGL)、「Texture3D」(Direct3D)、「sampler3D」(glsl/hlsl)など、直接的にAPIの単語を検索すると求めている情報が見つかりやすいでしょう。

3Dテクスチャの確保に最適なメソッドを使用しよう

TEXTURE_2D_ARRAYの記事で紹介したTIPSと同様に、下記の2点については3Dテクスチャにおいても有効です。適宜利用しましょう。

  • texImage3D()メソッドで一度に3Dテクスチャのすべての画像データを転送する代わりに、texSubImage3D()メソッドを使用して順次読み込み終わった画像から差分転送できる
  • メモリ効率・堅牢性の観点では、texImage3D()メソッドの代わりにtexStorage3D()メソッドを使用してイミュータブルな3Dテクスチャを確保してからtexSubImage3D()メソッドで画像データを転送したほうが良い

リファレンス

TEXTURE_3Dで使用するテクスチャ転送のメソッドについて、詳しくは下記のドキュメントを参照ください。