0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

p5.jsで生のwebglを使ってTextureを作って使ってみる

Last updated at Posted at 2025-01-28

はじめに

 今回で最後です。テクスチャの仕組みについて簡単に説明するのが目的です。ドーナツみたいなことをする方が仕組みを知るより楽しいのは分かります。ある意味当然ですね。パッと見で楽しい方がいいに決まっています。人生は短いので。楽しまないと。

グローバルステート

 グローバルステートの更新も今回で最後です。このモデルの原型はこちらのサイトです。
 WebGLで複数のテクスチャを使う
 このサイトの説明でようやくwebglの隠蔽された構造の自分なりのイメージを持つに至りました。それを進化させてVAOやTFOがようやく理解できるようになったので、いくら感謝してもしきれないです。

  globalState = {
    currentProgram: null,
    bindingBuffer: {
      arrayBuffer: null,
      elementArrayBuffer: null,
      uniformBuffer: null, ...
    },
    vertexAttributeArray:[
      {enable: false, /* 以下略 */}, {}, {}, ...
    ],
    bindingVertexArray: null,
    bindingTransformFeedback: null,
    bufferBase: [
      {transformFeedback:{}, uniform:{}}, {}, {}, ...
    ],
    activeTextureIndex: 0,
    textureSlot: [
      {texture:{'2D':null, 'CUBE':null, '2D_ARRAY':null, '3D':null}, sampler:null},
      {}, {}, {}, ...
    ],
    ... /* たくさん */
  }

 samplerは無視してください。説明しません。自分もよくわかってないです。こちらが参考になります。
 sampler
 activeTextureIndexとtextureSlotを追加しました。textureSlotとは、テクスチャ専用の部屋が16個並んでいるものです。それぞれの部屋には二段ベッドが2つずつ用意されています。四人部屋ですね。部屋番号を指定するのがactiveTextureIndexです。いわゆる「アクティブなテクスチャ」というのは、このインデックスで指定された部屋のことです。アクティブな部屋、です。その状況でbindTextureを実行すると、ターゲットの種類に応じて対象がこの部屋の住人としてセットされます。TEXTURE_2Dなら2Dのテクスチャとしてこの部屋に住人として登録されるわけです。

 部屋番号の変更はactiveTexture(index)で簡単に実行できます。たとえば2なら、2番の部屋のテクスチャがアクティブになるわけですね。それでなんかテクスチャ関連のメソッドを実行する場合、ターゲットで種類が特定され、インデックスで部屋番号が特定され、テクスチャが特定されるわけです。なおそこがnullであれば、何も起きません。

wdwf3v33v33.png

 注意があります。見ればわかるように同じ部屋に同じ種類のテクスチャは1つまでしか登録できません。他のテクスチャに対してbindTextureが実行された場合、上書きされます。違う種類であれば、同じインデックスであっても問題なく登録できます。たとえば0番に2DのテクスチャとCUBEのテクスチャが共存できるわけです。

  • TEXTURE_2D: 通常の2次元テクスチャ。キャンバスとかImageObjectが該当する。なおVideoElementもこれで扱う
  • TEXTURE_CUBE: いわゆるキューブマップ。6枚の画像を登録し、方向を元にピクセルデータを算出する
  • TEXTURE_2D_ARRAY: 複数枚の2Dテクスチャをまとめて扱う。z座標でそれぞれアクセスする。2Dでは実現不可能な枚数のテクスチャを配列のように扱える
  • TEXTURE_3D: いわゆるボリュームレンダリングに使う。shadertoyで時々見る

2Dだけ説明します。CUBEは3D描画なので扱えないです。残り二つは詳しくないです。

バインドする

 テクスチャを作ること自体はとても簡単です。

  const tex = gl.createTexture();

できました。で、登録はこうします。0番スロットに入れたいなら、

  gl.activeTexture(gl.TEXTURE0); // 0番を指定、グローバルステートに記録される。
  gl.bindTexture(gl.TEXTURE_2D, tex); // ターゲットを指定。ここの2D住人として登録。

でOK. 無事0番部屋の2D枠として登録できました。3番部屋にCUBEとして入れたいなら、

  gl.activeTexture(gl.TEXTURE3);
  gl.bindTexture(gl.TEXTURE_CUBE, tex);

簡単ですね。なお、activeTextureの引数ですが、
 gl.TEXTURE0, gl.TEXTURE1, ..., gl.TEXTURE15(たいてい16)
しか許されていません(もちろんこれらに該当する整数でもいいんですが、普通は、しない...)。部屋はほとんどの場合16です。常に8以上だそうです(後で知りましたが32のこともあるようです)。実はこれらは連番になっているので、3を欲しい場合、

  gl.activeTexture(gl.TEXTURE0 + 3);
  gl.bindTexture(gl.TEXTURE_CUBE, tex);

でもOKです。ていうか普通に整数でいいだろ...enableVertexAttribArrayだって整数じゃん...まあ、色々あるんだと思います。

中身を入れる

 texImage2Dを使います。2Dしかやらないので。実はCUBEもこれを使ったりします。

  gl.texImage2D(target, level, internalformat, width, height, border, format, type, pixels);

 たとえばこれ。pixelsにソースを入れます。覚え方があります。まず、最初はターゲットで最後はソース。これが基本です。今はTEXTURE_2Dしか使わないのでこれで始まります。最後はたとえばキャンバスのエレメントなんかが入ります。0と8が埋まりました。

 levelとborderは基本0です。1-5-ゼロの原則と覚えればいいです。1-5-ゼロの原則です。

 internalFormat, format, typeは組み合わせが限定されています。
 https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.6

wcwvwegvgw3e.png
cwcr2c3cv.png

2-6-7の制約と覚えればいいです。よく使うパターンも多くないです。通常のRGBAの場合、

  RGBA--RGBA--UNSIGNED_BYTE

でOKです。FLOATを使いたい場合、32bitのfloatのvec4であれば、

  RGBA32F--RGBA--FLOAT

を使えばいいです。HALF_FLOATの場合は

  RGBA16F--RGBA--HALF_FLOAT

で、せいぜいこのくらいしか使いません。

 空いた3-4には横幅と縦幅が入ります。そういうわけで、たとえば400x400の色テクスチャが欲しい場合、

  // 頭とおしりはターゲットとソース
  // 1-5-ゼロの原則で1-5番目はゼロ
  // 2-6-7の制約で2-6-7はRGBA,RGBA,UNSIGNED_BYTE
  // 3-4に400,400を指定
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 400, 400, 0, gl.RGBA, gl.UNSIGNED_BYTE, pixels);

ですね。pixelsのところにソースが入ります。直接ダイレクトに入れられるのは、

  • ImageData(よくわかんないです)
  • HTMLImageElement(new Image()で作るやつ)
  • HTMLCanvasElement(あとで詳述)
  • HTMLVideoElement(Videoです)
  • ImageBitmap(詳しくないです)

一番お世話になるのはHTMLCanvasElementだと思います。これはloadImage()で取得したデータを使う場合、

function preload(){
  img = loadImage("https://inaridarkfox4231.github.io/assets/texture/Shippei.jpg");
}

とかでやると、

  console.log(img.canvas instanceof HTMLCanvasElement);

がtrueになります。つまり.canvasでアクセスできます。またcreateGraphicsで作る場合、

  const gr = createGraphics(400, 400);
  console.log(gr.elt instanceof HTMLCanvasElement);

がtrueなので、.eltでアクセスできます。最後に、このキャンバス自体であれば、

  console.log(this.canvas instanceof HTMLCanvasElement);

がtrueになります。このキャンバス自体も使えるわけです。

 型付配列も指定できます。たとえば6x6でRGBAの色の場合、4x6x6個のバイトデータを用意すると、4つずつこの順で入って色になります。

  • Uint8Array(ただしtypeはgl.UNSIGNED_BYTE)
  • Float32Array(ただしtypeはgl.FLOAT)

で、あとは覚えなくていいと思います。
 次に、パラメータを設定します。

テクスチャパラメータ

 ところでtexImage2Dにはターゲットのテクスチャを示す引数がないですが、当然で、グローバルステート上で参照しているからです。ステートマシンですから。さっき上げた図のように、activeTextureで部屋番号を指定してターゲットで住人を指定しています。たとえば違う部屋にいくつかテクスチャを住まわせておけば、インデックスを切り替えるだけで操作対象をいじれるわけです。ただ16個までしか部屋はないため、多いと取り合いになったりします。譲り合いの精神が大事です。

 本題ですが、texImage2Dでテクスチャを入れたら決めないといけないパラメータがいくつかあります。このパラメータはテクスチャに属すため、グローバルステートは関係ありません。さっき述べたように、該当するテクスチャのパラメータをいじるものです。該当するテクスチャが見つからないと失敗します。使うのはtexParameteriです。これだけ覚えればいいです。自分もこれしか使いません。

 あと、重要なこととして、p5の機構を使うのではなく、こうして生で作る場合、フィルターは必ず指定しないといけないので注意してください。

フィルター

 MAG_FILTERとMIN_FILTERがあります。MAG_FILTERは拡大するとき、MIN_FILTERは縮小するときの補間方法を決めるっぽいです。正直よくわかんないしあんま意識しないです。とりあえず覚えるべきは、

  • FLOAT_TEXTUREを扱うなら基本NEAREST,NEAREST
  • 色テクスチャを扱うなら基本LINEAR,LINEAR
  • 色でもなんかモザイクとかしたいんならNEARESTを指定することもあるかも?

以上です。決めるには、

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST/*またはgl.LINEAR*/);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST/*またはgl.LINEAR*/);

とかそんな感じですね。これ以外はp5も使ってないので問題ないです。

ラップ

 テクスチャにシェーダー内でフェッチするにはtextureという関数を使うんですが、範囲は0~1で指定することになっています。x,yいずれもです。vec2の引数を入れるんですが、これがはみ出した場合の挙動を決めるのがTEXTURE_WRAPです。SとTで横と縦それぞれ指定できるようになっています。

  • REPEATにするとmodを取って小数にして参照する
  • CLAMP_TO_EDGEにすると0~1に文字通りクランプして指定する。つまり同じ値がびょ~んってなる
  • MIRRORED_REPEATにすると0~1が1~0となって、つまり鏡写しで値が切れずに延々と続く

 なお実は、p5はラップのデフォルトをCLAMP_TO_EDGEに指定しています。まあいじればいいんですが。デフォルトを決める権利はユーザーにはありません。

 指定するのはこんな感じ:

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);

 これ以外にMIPMAPとかいろいろあるんですが、p5はMIPMAPやってないし、自分も必要としていないので、説明しません。気になったら調べればいいんです。

フロートテクスチャで実験してみる

 試しにフロートのテクスチャを入れてみます。中身を取り出して確かめるところまで行きます。
 Float Texture Input

// フロートテクスチャのサンプル

function setup() {
  createCanvas(400, 400, WEBGL);

  const gl = this._renderer.GL;

  // フロートテクスチャの中身を取り出すプログラム
  const vsTFF =
  `#version 300 es
   uniform sampler2D uTex;
   uniform vec2 uSize;
   out vec4 vOut;
   void main(){
     float fi = float(gl_VertexID);
     float x = mod(fi, uSize.x);
     float y = floor(fi / uSize.x);
     x = (x + 0.5) / uSize.x;
     y = (y + 0.5) / uSize.y;
     vOut = texture(uTex, vec2(x, y));
   }
  `;
  const fsTFF =
  `#version 300 es
  void main(){}
  `;
  const pgTFF = createShaderProgram(gl, {
    vs:vsTFF, fs:fsTFF, outVaryings:["vOut"]
  });

  texTest0(gl, pgTFF);
}

function texTest0(gl, pg){
  const tex = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tex);

  // 0~23を入れる。
  const faIn = new Float32Array(24);
  for(let i=0; i<24; i++){ faIn[i] = i; }

  // 加えてこれも必要。プレ丸を切る。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
  // 2x3に格納する
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, faIn);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

  // これやんないと駄目。
  // ピンポイントで値にフェッチする場合は必須。
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

  // 中身を確認する
  const buf = getBuf(gl, 96);
  gl.useProgram(pg);

  setUniformValue(gl, pg, "1i", "uTex", 0);
  setUniformValue(gl, pg, "2f", "uSize", 2, 3);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
  gl.beginTransformFeedback(gl.POINTS);
  gl.enable(gl.RASTERIZER_DISCARD);
  gl.drawArrays(gl.POINTS, 0, 6);
  gl.disable(gl.RASTERIZER_DISCARD);
  gl.endTransformFeedback();
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
  gl.flush();

  // bufの中身を確認する。
  const faOut = new Float32Array(24);
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.getBufferSubData(gl.ARRAY_BUFFER, 0, faOut);
  console.log(faOut); // 1,2,3,4
}

// byteSizeバイトの領域確保とWBOの取得
function getBuf(gl, byteSize){
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, byteSize, gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  return buf;
}
/* 以下略 */

実行するとfaOutに0,1,2,3,...,23が入ってることを確認できます。
 まず言い忘れてたんですが、p5はRGBAのフォーマットの場合、格納時にALPHAというか第四成分をrgbに掛け算する仕組みを採用しています。上のコードで、

  // 加えてこれも必要。プレ丸を切る。
  //gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
  // 2x3に格納する
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, faIn);
  //gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

とかするとめちゃくちゃな数字が格納されます。いわゆるプレ丸といって、アルファチャンネルを予めRGBに掛け算しておくわけですね。これをやることで画像テクスチャの扱いの際のアルファブレンドが簡略化されるのでそれを狙っているわけです。後でわかると思いますが...ともかく、フロートを正確にぶち込みたい場合に不便なので、意図的に切っています。なおデフォルトではこれはfalseになっています。ただいつでもいじれるので、自由に決めましょう。

 今やっているwebglに関するあれも、これも、向こう(開発サイド)が勝手に決めてしまっている部分に対し、こっちが何らかの自由を獲得したい場合にどうすればいいかのヒントを提供するため(というかむしろ自分がそれをやるため)という狙いが、無くはないです。これなんか変だなと思っても、知識がなければ何にもできません。

 まずFloat32Arrayで0~23の24個の数字を格納し、2x3のサイズで格納します。

  // 2x3に格納する
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, faIn);

これだと中身が分かりません。実はこの中身を取得するには一般にフレームバッファという技術と、readPixelsという関数を組み合わせます。長い道のりになるので、面倒なのでやりません。代わりに前回やったTFFを使ってWBOに落として、getBufferSubDataで取得する方針とします。
 その前に、フィルターをNEARESTにしておきます。

  // これやんないと駄目。
  // ピンポイントで値にフェッチする場合は必須。
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);

96バイトの領域を確保します。

// byteSizeバイトの領域確保とWBOの取得
function getBuf(gl, byteSize){
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, byteSize, gl.STATIC_DRAW);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  return buf;
}

シェーダー。

vs
#version 300 es
uniform sampler2D uTex;
uniform vec2 uSize;
out vec4 vOut;
void main(){
  float fi = float(gl_VertexID);
  float x = mod(fi, uSize.x);
  float y = floor(fi / uSize.x);
  x = (x + 0.5) / uSize.x;
  y = (y + 0.5) / uSize.y;
  vOut = texture(uTex, vec2(x, y));
}

 なお言い忘れてましたが、テクスチャをuniformで使うには整数を指定します。これは普通に部屋番号です。0なら0番部屋のテクスチャが使われます。種類を指定しなくていいのはなぜかというと、glslサイドで型により判断しているからです。

  • 2D: sampler2D
  • CUBE: samplerCube
  • 2D_ARRAY: sampler2DArray
  • 3D: sampler3D

部屋番号と種類で、めでたく特定できるわけです。で、このコードではサイズも指定したうえで、渡されたインデックスに従って2次元座標を特定し、そこの「色」をvec4でフェッチして取得しています。ほんとは整数で指定する方法もあるんですが、面倒なので割愛します。0.5を使って真ん中に来るよう気を付けると、変な事故を防げます。

 TFFなのでoutVaryingsを指定します。

  const pgTFF = createShaderProgram(gl, {
    vs:vsTFF, fs:fsTFF, outVaryings:["vOut"]
  });

で、まあTFO要らないんで、普通にやります。なお普通にやる場合、bindBufferBaseで先に受け皿を指定してからbeginしないとエラーになるので注意しましょう。

  setUniformValue(gl, pg, "1i", "uTex", 0);
  setUniformValue(gl, pg, "2f", "uSize", 2, 3);
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
  gl.beginTransformFeedback(gl.POINTS);
  gl.enable(gl.RASTERIZER_DISCARD);
  gl.drawArrays(gl.POINTS, 0, 6);
  gl.disable(gl.RASTERIZER_DISCARD);
  gl.endTransformFeedback();
  gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);

ピクセルは6つなので0,6を指定しています。これでセットしたbufに数が入ります。4つずつvec4で2x3に並べてあるのであれば、正しく格納されているはずです。それをgetBufferSubDataで取得します。

  // bufの中身を確認する。
  const faOut = new Float32Array(24);
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.getBufferSubData(gl.ARRAY_BUFFER, 0, faOut);
  console.log(faOut); // 1,2,3,4

長さ24のFloat32Arrayでいいですね。ちゃんと入っていますね。これで確認できました。くれぐれもフィルター設定を忘れないでください。

色のテクスチャを入れてみよう

 次に、createGraphicsでキャンバスを作って利用してみます。
 COLOR TEXTURE

// 色テクスチャのサンプル

function setup() {
  createCanvas(400, 400, WEBGL);

  const gl = this._renderer.GL;

  // フロートテクスチャの中身を取り出すプログラム
  const vsCOLOR =
  `#version 300 es
   layout (location = 0) in vec2 aPosition;
   out vec2 vUv;
   void main(){
     vec2 p = aPosition;
     vUv = 0.5 * p + 0.5;
     vUv.y = 1.0 - vUv.y;
     gl_Position = vec4(p, 0.0, 1.0);
   }
  `;
  const fsCOLOR =
  `#version 300 es
  precision highp float;
  in vec2 vUv;
  out vec4 fragColor;
  uniform sampler2D uTex;
  void main(){
    vec4 tex = texture(uTex, vUv);
    fragColor = tex;
  }
  `;
  const pgCOLOR = createShaderProgram(gl, {
    vs:vsCOLOR, fs:fsCOLOR
  });

  // 0番にあれを入れておく
  const ua = new Int8Array([-1,-1, 1,-1, -1,1, 1,1]);
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, ua, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.BYTE, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  texTest0(gl, pgCOLOR);
}

function texTest0(gl, pg){
  const tex = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tex);

  const gr = createGraphics(400, 400);
  gr.textSize(110);
  gr.fill(255);
  gr.textAlign(CENTER,CENTER);
  gr.textStyle(ITALIC);
  gr.text("texture", 200, 200);

  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 400, 400, 0, gl.RGBA, gl.UNSIGNED_BYTE, gr.elt);

  // かならず指定する。色なのでLINEAR.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  gl.clearColor(0,0,0.5,1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  
  gl.useProgram(pg);
  setUniformValue(gl, pg, "1i", "uTex", 0);

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  gl.flush();
}
/* 以下略 */

 予め頂点アトリビュートの0番には板の頂点を入れておきます。一つしかないのにロケーションを設定しているのは懲りたからです。ちなみにこういうのは1回だけセッティングを作っておいて...

  const vaoForBoard = gl.createVertexArray();
  gl.bindVertexArray(vaoForBoard);
  const ua = new Int8Array([-1,-1, 1,-1, -1,1, 1,1]);
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, ua, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.BYTE, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindVertexArray(null);

こうしてvaoにぶち込んでいつでも呼び出せるようにしておくと便利です。プログラムがゲーム機なら、vaoはゲームソフトのようなものです。お気に入りのソフトを作りましょう。
 ちなみにシェーダーの内容は要するにUVを作って補間して使うためのものです。上下反転している理由は後で説明します。まず、

  const tex = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tex);

  const gr = createGraphics(400, 400);
  gr.textSize(110);
  gr.fill(255);
  gr.textAlign(CENTER,CENTER);
  gr.textStyle(ITALIC);
  gr.text("texture", 200, 200);

  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 400, 400, 0, gl.RGBA, gl.UNSIGNED_BYTE, gr.elt);

  // かならず指定する。色なのでLINEAR.
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

0番を開けておいて、登録して、createGraphicsでなんか作って、eltでキャンバス要素にアクセスして、400x400で画像データを登録します。そのあとフィルターを掛けています。フィルターは必須です。これをしないと何にも表示されません。今回は色なのでLINEARを指定しています。
 実は、画像データにおいては左上が(0,0)になっています。ゆえに正規化デバイス座標からフェッチする場合、単純に0.5倍して0.5を足すと上下が逆になってしまいます。それでyだけ反転させています。

vs
#version 300 es
layout (location = 0) in vec2 aPosition;
out vec2 vUv;
void main(){
  vec2 p = aPosition;
  vUv = 0.5 * p + 0.5;
  vUv.y = 1.0 - vUv.y;
  gl_Position = vec4(p, 0.0, 1.0);
}

で、あとはフェッチするだけ。

#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uTex;
void main(){
  vec4 tex = texture(uTex, vUv);
  fragColor = tex;
}

なんですが、ここでちょっと難しい問題があります...
 まず今回のドローコールでは背景色をネイビーにしてあります。そのため実行結果は:

bdhhgeudbhj.png

このようになるんですが、これはBLENDによるものです。

  gl.clearColor(0,0,0.5,1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  
  gl.useProgram(pg);
  setUniformValue(gl, pg, "1i", "uTex", 0);

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  gl.flush();

 なぜなら今回は画面全体を覆う板を描画しているからです。文字のところだけポリゴンで描画しているわけではありません。そのためアルファ値が問題になってきます。一般に、アルファブレンドはどうするかというと、srcが重ねる側、dstが重ねられる側だとして、

  rgb = src.a * src.rgb + (1.0-src.a) * dst.rgb,
  a = src.a + (1.0 - src.a) * dst.a

のようにします。今、textureにおいてrgbとaが元の画像で分離されているとすると、本来指定すべきブレンドというのは、

  gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

です。セパレートの場合、まずrgbの係数を指定して、それからアルファの係数を指定します。
 blendFuncSeparate()
ただまあ面倒なんですよね。そこでプレ丸の出番です。

  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

p5はこれを事前にやっています。そのおかげで、あらかじめrgbにはtexImage2Dの実行時にalphaが掛けられています。すると取得時も「そういう」値が取得されるため、上の式でsrc.aとsrc.rgbの積はsrc.rgbがそのままそういう値になっており、

  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

で済むわけです。そういうことです。なおBLENDはデフォルトでは切ってあるので有効化します。管理はユーザーに委ねられています。たとえばフロートテクスチャの場合、ほとんどの場合上書きですから、BLENDは基本的に切っておくべきでしょうが、ONE,ONEにして加算させたりする場合もまれにあります。

 また、さっき述べたようにプレ丸はフロートテクスチャでRGBAをやる場合も適用されます。注意です。

 なお、プレ丸を切るとこんな見た目になります。

v33fd3r3f3.png

なんかギザギザして気持ち悪いですね。この場合、シェーダーサイドでalphaをrgbに掛けると見た目が復元されます。

fs
#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uTex;
void main(){
  vec4 tex = texture(uTex, vUv);
  tex.rgb *= tex.a;
  fragColor = tex;
}

まあそういうのが面倒なので、プレ丸を採用してるわけですね。

loadImageで取得した画像を使う場合

 さっきのサンプルでgr.eltをimg.canvasにするだけであと全部いっしょなので割愛します。

あとは...

 webGLBufferからテクスチャを作る方法があるようです。ステートマシンよろしく、あそこのbindingBuffer枠の、ターゲットをgl.PIXEL_UNPACK_BUFFERとすることで埋まるのがあるんですが、そこに置いたwebGLBufferを入れられる仕組みがあるようです。詳しくは分かんないです。

自由に部屋を使おう

 テクスチャの部屋、というかスロットは全部で16個あります。i番目の部屋に入れるには、

  gl.activeTexture(gl.TEXTURE0 + i);
  gl.bindTexture(gl.TEXTURE_2D, texture);

とするだけですから簡単です。あとはuniformで整数を指定すればsampler2Dでアクセスできます。簡単です。テクスチャの流儀には、あらかじめ必要なテクスチャを別々の部屋にぶち込んでおいて、uniformで必要なプログラムに紐付けて使用するやり方と、その都度プログラムが使う番号の部屋に必要なテクスチャを割り当てて、nullバインドで戻して、とするやり方があります。どっちが便利かはスケッチ次第で、正解はありません...が、p5は後者のやり方を徹底しています。そもそもテクスチャを設定する関数が
 texture()
しかないので、別々の部屋に割り当てるなんていう贅沢はそもそもユーザーには許されていません。

 しかし自由にテクスチャを使う方法は、一応、あります。その時のプログラムをレンダラーから取得して、空いてる部屋に使いたいテクスチャをぶち込み、プログラムにその番号をuniformでセットしておけば、リセットされないので自由に使えます。他の部屋にも入れておいて、整数の差し替えで入れ替えてもいいし、そのスポットに好きなテクスチャを取っかえ引っ換え、でもいいですね。自由に自分の責任で管理できます。

 特にカスタムシェーダーの場合、テクスチャが衝突しやすいので、p5サイドが使わない番号をうまく特定すれば衝突により生じるバグを防げる可能性があります。テクスチャに関して独自でなんか書いてるときに引っかかったら、そういう選択肢があることを覚えておくといいと思います。p5のテクスチャ管理は煩雑すぎて手に負えないので、逃げ道を確保しておくと自由になれる場合があります。

 例えば実例ですが、カスタムシェーダでsetUniformによりテクスチャを登録すると、描画のたびにリセットされます。

let img;
let sh;

const vs =
`#version 300 es
in vec3 aPosition;
out vec2 vUv;
uniform float uShift;
void main(){
  vUv = aPosition.xy + 0.5;
  vUv.y = 1.0 - vUv.y;
  gl_Position = vec4(uShift + aPosition.xy * 2.0, 0.0, 1.0);
}
`;

const fs =
`#version 300 es
precision highp float;
uniform sampler2D uTex;
in vec2 vUv;
out vec4 fragColor;
void main(){
  vec4 tex = texture(uTex, vUv);
  fragColor = tex;
}
`

function preload(){
  img = loadImage("https://inaridarkfox4231.github.io/assets/texture/Shippei.jpg");
}

function setup() {
  createCanvas(400, 400, WEBGL);
  sh = createShader(vs,fs);
}

function draw() {
  shader(sh);
  clear();
  noStroke();
  sh.setUniform("uShift", 0);
  sh.setUniform("uTex", img);
  plane(0);
  //sh.setUniform("uTex", img);
  sh.setUniform("uShift", 0.5);
  plane(0);
}

結果:

evegg44.png

plane(0)で描画される理由はアトリビュート変数をダイレクトに使っているからです。uShiftでずらしているのにおかしいですね。描画されていません。ちなみにコメントアウトを外して描画のたびにsetUniformするよう指定すると、

ef3tf3g54555.png

ちゃんと複製されます。このsetUniformでテクスチャを登録する処理というのがひどく煩雑で、少なくとも自分には摩訶不思議すぎて解明できませんでした。そこで、

 頑張ったんですが、ダメでした。

以上です。

 追記:反則をやればできます。

function setup() {
  createCanvas(400, 400, WEBGL);

  sphere(10);
  sh = createShader(vs,fs);

  // こうすればいい
  sh.unbindTextures = () => {};
}

 いつだったかunbindTextures()の仕様が変更されたんですよね。それでできなくなったようです。

 ここですね。
 Unbind textures when done with a shader #5923
 そしてこれにより問題が生じることはすでにキャッチされています。
 Image uniforms get reset after each draw using a shader #7030
 この問題は現在も未解決のようです。書いてありますが、今年中に解決するつもりらしいです。
 Fixed uniform shader reset #7207
 なお、引き金を引いたのはp5.Framebufferです。あとは言わなくてもなんとなく察せられると思います。誰だって自分が実装した仕様には誇りを持ちたいわけです。どんなにアレな仕様でも。つまりそういうことですね。

 そういうわけで、いろいろ言ったんですが、プログラムの取得などいろんな困難が立ちはだかるので無理かもしれないですね。
 ここまで記事を読んできた人は分かると思いますが、実行するのは「プログラム」です。シェーダーではないです。ユニフォームの登録のみならず、あらゆるあれやこれがプログラムベースです。それが容易に取得できない以上、自由にできることは何もありません。素直に従うしかなさそうです。
 p5サイドである時期開発していて感じたこととしては、p5サイドはwebGLに関してはユーザーがレンダラーGL、要するに今まで使ってきた「生の」レンダラーであれこれやるのをあんま快く思ってない節があるので、そういう風になっているんだと思います。

 上の問題について補足します。これは、言っても仕方ないんですが、言わずにはいられないので言います。

自分で管理できれば一番良い

 ということです。何が話をややこしくしているかと言えば、この記事で紹介されているwebGLのテクスチャの仕組みを誰も理解していない、もしくは理解しようとしないということを前提として、テクスチャの取り扱いのシステムを作らなければならないという前提です。なぜなら上から勝手に与えられるテクスチャは存在しないからです。すべてのテクスチャは、そのスケッチに必要であるがゆえに、こちらで、自前で用意するのです。そうであれば、いつどこで何のために使うのか、また破棄するのか、すべてこっちの自由に決められるのが一番自然ではないでしょうか。それも仕組みを理解していれば整数であれこれ指定するだけの単純作業です。しかし知らないことが前提で組み立てなければならない故に、あらゆる問題が発生してしまいます。結局一番最初に仕組みを作った人の責任ですが、もうその人はどこにもいません。今更システムをひっくり返すこともできません。めんどくさいですね。

 とはいえ

 単純指定でしか使わないなら何も問題は発生しません。上でも述べたように関数はtexture()しかない。p5はそもそもマルチテクスチャを想定していないし、複雑な取り扱いを想定していません。お手軽3Dがp5の個性です。p5らしく使いましょう。変なことは考えないのが一番ですね。

いくつかの補足

devicePixelRatioが1より大きい場合

 devicePixelRatioというかpixelDensity()で取得するあれですが、あれが1より大きいと、上に挙げた色テクスチャのコードは失敗します。というのもtexImage2Dでは実際のデータ量を指定するからです。具体的にはtypeのバイト長にピクセル数のニ乗を掛けたものです。たとえば自分のスマホは2.625なので3になります。なので、さっきのコードをスマホで実行したら「texture」の文字が消えてしまいました。登録に失敗しているからです。
 bufferDataの項で説明したように、データを送る際は基本的に全部送れないとエラーになるからです。
 解決法の一つはpixelDensity(1);を明示することです。

function setup() {
  createCanvas(400, 400, WEBGL);
  pixelDensity(1);

もうひとつはcreateGraphics()で得られるデータ量に見合った受け皿を用意することです。それはpixelDensity()で得られる値をそれぞれ掛ければいいです。

  const density = pixelDensity();
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, density*400, density*400, 0, gl.RGBA, gl.UNSIGNED_BYTE, gr.elt);

 しかし一番汎用性が高いのは、ソースがCanvasElementの場合はそこから得られるwidthとheightを使うことですね。devicePixelRatioが大きい場合、ここが800や1200になります。

  const src = gr.elt;
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, src.width, src.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);

p5はこのやり方でやってるはずです。

ラップパラメータ

 今回はロード画像を使います。

let img;
function preload(){
  img = loadImage("https://inaridarkfox4231.github.io/assets/texture/Shippei.jpg");
}

指定にはwidthとheightをそのまま使います。

  const src = img.canvas;
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, src.width, src.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);

シェーダをちょっとだけいじります。

fs
#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform sampler2D uTex;
void main(){
  vec4 tex = texture(uTex, vUv*4.0); // 4倍
  fragColor = tex;
}

そのまま実行すると、こう。

repeat.png

デフォルトのラッピングはリピートだからです。これを...

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

とすると

clamp.png

まあ画像で使うラッピングではないですね(じゃあやるなや)。ちなみに境界上の色を画像処理で特定の色にすることで外側を単色にしたり、あるいはイレースで消すことにより外側の透明度を0にしたり出来ます。使い方次第ですね。
 次に、ミラーでは

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);

mirror.png

こうなります。幾何学模様と相性が良さそうです。境界がつながっているのがいいですね。
 MIRRORED_REPEAT

TFFが本に出てこないことについて

 トランスフォームフィードバックについて知りたかったのでオライリーのヤドクガエルの本を買ったんですが、肝心のトラフィーは381ページで5行、触れられているだけでした。なんていうか、期待外れですね...それでもいろいろ勉強になる本なのでそのうちきちんと読もうと思います。
 難しいっていうのはやり方が分かんないか、もしくはそもそも必要としていないからだと思っています。今回はテクスチャの内容をwebGLバッファに焼くためサクッと使いましたが、こんな感じでおもちゃ遊びすることによりハードルを下げる過程が必要なんだと思います。フレームバッファの方がよっぽど難しいと思います。まあ便利なんですが。ざっくり言うとここで紹介したテクスチャにドローコールによりデータを焼く技術です。トラフィーはwebGLバッファが対象でしたが、こちらはテクスチャが対象なので、数でも、絵でも、何でもありです。

webGLバッファからテクスチャを作る

 フロートテクスチャで作ったサンプルの

  // 0~23を入れる。
  const faIn = new Float32Array(24);
  for(let i=0; i<24; i++){ faIn[i] = i; }

  // 加えてこれも必要。プレ丸を切る。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
  // 2x3に格納する
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, faIn);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

ここを次のように書き換えます。

  // 0~23を入れる。
  const faIn = new Float32Array(24);
  for(let i=0; i<24; i++){ faIn[i] = i; }

  const inBuf = gl.createBuffer();
  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, inBuf);
  gl.bufferData(gl.PIXEL_UNPACK_BUFFER, faIn, gl.STATIC_DRAW);

  // 加えてこれも必要。プレ丸を切る。
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, false);
  // 2x3に格納する
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, 0);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

9番目の引数が0になっています。ここを整数で指定するのは、PIXEL_UNPACK_BUFFERという種類のwebGLバッファがグローバルステートに存在するとき許され、その場合バイトデータとしてこれが使われます。これはオフセットなんですがちょっと仕様が厄介で...後述します。このコードの結果は同じなので割愛します。また、データを32個用意して、


  // 0~23を入れる。
  const faIn = new Float32Array(32);
  for(let i=0; i<32; i++){ faIn[i] = i; }

  /* 略 */
  
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA32F, 2, 3, 0, gl.RGBA, gl.FLOAT, 32);
  gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, true);

  gl.bindBuffer(gl.PIXEL_UNPACK_BUFFER, null);

とすると8,9,10,11,...,31が格納されます。おそらくアトポンと一緒で、オフセットがデータタイプのバイトの倍数でないと失敗します。また0,4,8,12では必ず最初からで、16,20,24,28で4,5,6,7,...と入ります。おそらくはじめっから16バイトずつの区切りで、切り捨てで最終的なオフセットが決まるようです。ただその理屈だと36でエラーになる理由が...結局32バイトでギリギリなのでそれは越えちゃいけないのでしょう。難しいですね。

 まあ、素直にピクセルごとのバイト数の倍数で指定するべきでしょう。この場合だと16ですね。それなら素直な結果になりますね。

 普通に型付配列を入れた方が早いのは明らかです。これが機能するのはARRAY_BUFFERと兼用する場合でしょうか。実はARRAY_BUFFER枠に一度入れたバッファであっても、この枠に入れることが可能なようです(ただしELEMENT_ARRAY_BUFFERに入れたことがある場合は不可...あそこに入る場合だけ重複不可能の仕様が発動するようです)。なのでARRAY_BUFFERの、要するにVBOですが、VBOをテクスチャに焼きたい場合は便利かもしれないですね。

 もしくは、今回はやりませんでしたが、VBOをTFFかなんかで更新して、その結果でテクスチャを更新する場合など、この方法でないといけないかもしれないですね。元の配列はすでに破棄されてる場合が多いので、その場合は直接VBOを使った方がどう考えても手っ取り早いです(というか無理...)。

 前回のTFFのコードも、VBOとテクスチャでキャッチボールする感じに書けるかもしれないですね。興味のある人はやってみましょう(自分でやってみました。一応できたんですが、texSubImageの処理が重すぎて4096で既にパフォーマンスが怪しくなったのでやめました。こんなので10万個とか動かしたらパソコンが死んでしまう...一時的というか、不定期で実行する場合に限られる感じですかね)。

テクスチャスロットを16個使ってみる

 16個使ってみましょう。色テクスチャのコードで、フラグメントをこうします(処理系によっては32個が上限であることを後で知りました。そういうわけでタイトルを変更しました)。

fs
  #version 300 es
  precision highp float;
  in vec2 vUv;
  out vec4 fragColor;
  uniform sampler2D uTex;
  uniform vec2 uOffset;
  void main(){
    vec4 tex = texture(uTex, vUv*4.0 - uOffset);
    fragColor = tex;
  }

いわゆるテクスチャシフトですね。で、

  const grs = [];
  for(let i=0; i<16; i++){
    const gr = createGraphics(100, 100);
    gr.textSize(60);
    gr.textAlign(CENTER, CENTER);
    gr.textStyle(ITALIC);
    gr.fill(255);
    gr.text(i, 50, 50);
    grs.push(gr);
  }

  for(let i=0; i<16; i++){
    const tex = gl.createTexture();
    gl.activeTexture(gl.TEXTURE0 + i);
    gl.bindTexture(gl.TEXTURE_2D, tex);
    const src = grs[i].elt;
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, src.width, src.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);
    // かならず指定する。色なのでLINEAR.
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  }

  gl.activeTexture(gl.TEXTURE0); // 別にしなくてもいい

  gl.clearColor(0,0,0.5,1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
  
  gl.useProgram(pg);

  gl.enable(gl.BLEND);
  gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);

  for(let y=0; y<4; y++){
    for(let x=0; x<4; x++){
      setUniformValue(gl, pg, "2f", "uOffset", x, y);
      setUniformValue(gl, pg, "1i", "uTex", x+4*y);
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
    }
  }

0番から15番に番号の付いたテクスチャを入れます。クランプで外側の透明度をなくしておきます。結果は:

wfewvwffffff3.png

 綺麗に並びました。いっぱい使うと気持ちがいいですね!もちろん普通は描画のたびにスロットを入れ替えるんですが、まあいちいちバインドして解除するのは面倒ですね...でも普通はそうします。部屋は少ないので、譲り合いましょう。

同じスロットにある複数のテクスチャを同じプログラムで使うと失敗する

 たとえば最初に説明したように、同じスロットに違うタイプのテクスチャを共存させることは可能です。次のコードを実行すると、全部入ってるのが分かります。

  const gl = this._renderer.GL;
  const targets = [
    gl.TEXTURE_2D, gl.TEXTURE_CUBE_MAP,
    gl.TEXTURE_2D_ARRAY, gl.TEXTURE_3D
  ];
  const bindTargets = [
    gl.TEXTURE_BINDING_2D, gl.TEXTURE_BINDING_CUBE_MAP,
    gl.TEXTURE_BINDING_2D_ARRAY, gl.TEXTURE_BINDING_3D
  ];
  // 0番に全部入れる
  gl.activeTexture(gl.TEXTURE0);
  for(let i=0; i<4; i++){
    const tex = gl.createTexture();
    gl.bindTexture(targets[i], tex);
  }
  // 全部true
  for(let i=0; i<4; i++){
    console.log(gl.getParameter(bindTargets[i]) !== null);
  }

ですが、プログラムで同じところに入ってる複数のテクスチャを使用すると失敗します。具体例:

/*
  1.同じスロットの複数のテクスチャは使えない
  2.複数のテクスチャを使う場合はスロットを分ける
*/

// 今回は0番と1番に2D, 0番に2D_ARRAYを入れる。

function setup() {
  createCanvas(400, 400, WEBGL);

  const gl = this._renderer.GL;

  // tex2Dとtex2DArrayを併用する
  const vsCOLOR =
  `#version 300 es
   layout (location = 0) in vec2 aPosition;
   out vec2 vUv;
   void main(){
     vec2 p = aPosition;
     vUv = 0.5 * p + 0.5;
     vUv.y = 1.0 - vUv.y;
     gl_Position = vec4(p, 0.0, 1.0);
   }
  `;
  const fsCOLOR =
  `#version 300 es
  precision highp float;
  precision mediump sampler2DArray;
  in vec2 vUv;
  out vec4 fragColor;
  uniform sampler2D uTex_2D;
  uniform sampler2DArray uTex_2DArray;
  uniform float uColorId;
  void main(){
    vec4 tex0 = texture(uTex_2D, vUv);
    vec4 tex1 = texture(uTex_2DArray, vec3(vUv, uColorId));
    vec3 col = tex0.a * tex0.rgb + (1.0-tex0.a)*tex1.rgb;
    float alpha = tex0.a + tex1.a - tex0.a*tex1.a;
    fragColor = vec4(col, alpha);
  }
  `;
  const pgCOLOR = createShaderProgram(gl, {
    vs:vsCOLOR, fs:fsCOLOR
  });

  // 0番にあれを入れておく
  const ua = new Int8Array([-1,-1, 1,-1, -1,1, 1,1]);
  // VAO化しよう
  const boardVAO = gl.createVertexArray();
  gl.bindVertexArray(boardVAO);
  const buf = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buf);
  gl.bufferData(gl.ARRAY_BUFFER, ua, gl.STATIC_DRAW);
  gl.enableVertexAttribArray(0);
  gl.vertexAttribPointer(0, 2, gl.BYTE, false, 0, 0);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);
  gl.bindVertexArray(null);

  // 2Dを2枚用意する
  for(let i=0; i<2; i++){
    gl.activeTexture(gl.TEXTURE0 + i);
    const tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, tex);
    const gr = createGraphics(400, 400);
    gr.textSize(320);
    gr.textAlign(CENTER, CENTER);gr.textStyle(ITALIC);
    gr.fill(255);gr.text(i, 200, 200);
    const src = gr.elt;
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, src.width, src.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  }
  // 2D_Arrayを用意する
  const gr2 = createGraphics(8, 32);
  gr2.noStroke();
  gr2.fill("red");gr2.rect(0,0,8,8);
  gr2.fill("green");gr2.rect(0,8,8,8);
  gr2.fill("blue");gr2.rect(0,16,8,8);
  gr2.fill("orange");gr2.rect(0,24,8,8);
  const tex2DArray = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex2DArray);
  const src = gr2.elt;
  gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, src.width, floor(src.height/4), 4, 0, gl.RGBA, gl.UNSIGNED_BYTE, src);
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  gl.useProgram(pgCOLOR);
  setUniformValue(gl, pgCOLOR, "1i", "uTex_2D", 1);
  setUniformValue(gl, pgCOLOR, "1i", "uTex_2DArray", 0);
  // 0,1,2,3で色を指定します。
  setUniformValue(gl, pgCOLOR, "1f", "uColorId", 0);

  // nullでも関係ない。指定されていることが問題。
  //gl.activeTexture(gl.TEXTURE0);
  //gl.bindTexture(gl.TEXTURE_2D_ARRAY, null);

  gl.clearColor(0.2, 0.2, 0.2, 1);
  gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);

  // エラーが発生するのは描画時。
  gl.bindVertexArray(boardVAO);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  gl.bindVertexArray(null);
}

function createShaderProgram(gl, params = {}){
  /* 以下略 */

別のところで2D_ARRAYを説明したようなのでそれを使っています。CUBEとかは使えないんで。で、このプログラムでは2Dを0,1に1枚ずつ、2D_ARRAYを0に置いています。ユニフォームで両方を0にすると、セット時は何にも言われないんですが、ドローコールの実行時にエラーを出されます。

  setUniformValue(gl, pgCOLOR, "1i", "uTex_2D", 0);
  setUniformValue(gl, pgCOLOR, "1i", "uTex_2DArray", 0);
  /* 略 */
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

GL_INVALID_OPERATION: Two textures of different types use the same sampler location.

これは同じスロットから複数のテクスチャを使用した場合に出されるエラーです。回避することはできないので、複数種類のテクスチャを使う場合は素直にスロットを分けましょう。同じスロットに同じ種類は1つまでですから、結局のところ種類に依らず同じプログラムで使えるテクスチャは16枚までということです。2D_ARRAYの強力さが分かりますね。

  setUniformValue(gl, pgCOLOR, "1i", "uTex_2D", 1);
  setUniformValue(gl, pgCOLOR, "1i", "uTex_2DArray", 0);

実行結果:

red.png

おわりに

 TFFとテクスチャでモヤモヤしてる部分を解消するという当初の目的はある程度達したので、webGLのアウトプットは終わりです。
 
 
 ここまでお読みいただいてありがとうございました。

0
1
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
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?