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でカスタムシェーダーを使うと描画のたびにテクスチャがリセットされるのを回避するためにTEXTURE_2D_ARRAYを使う

Last updated at Posted at 2025-01-30

はじめに

 p5.jsでテクスチャを使う場合、一回だけ呼び出しておけば、基本的にその内容は継続して適用され続けます。具体例を挙げます。

// 通常は描画のたびにリセットされることはないですね。

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

  const gr = createGraphics(100, 100);
  gr.textAlign(CENTER, CENTER);
  gr.textSize(80);
  gr.fill(255);
  gr.text("", 50, 50);

  noStroke();
  background(0, 0, 128);

  texture(gr);

  for(let x=-1; x<2; x++){
    for(let y=-1; y<2; y++){
      push();
      translate(x*100, y*100, 0);
      plane(100);
      pop();
    }
  }
}

実行結果:

tuujouCase (1).png

ここまでは特に問題ないですね。しかし、baseMaterialShaderなどを使ってカスタムシェーダーの枠組みで同じことをすると、ちょっと変な結果になります。具体例を挙げます。

// カスタムケース
// カスタムでuTexというsampler2Dを用意する。
// 残念でした。

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

  const gr = createGraphics(100, 100);
  gr.textAlign(CENTER, CENTER);
  gr.textSize(80);
  gr.fill(255);
  gr.text("", 50, 50);

  const sh = baseMaterialShader().modify({
    'fragmentDeclarations':`
      uniform sampler2D uTex;
    `,
    'vec4 getFinalColor':`(vec4 color){
      color = texture(uTex, vTexCoord);
      return color;
    }`
  });

  noStroke();
  background(0, 0, 128);

  shader(sh);

  //texture(gr);
  sh.setUniform("uTex", gr);

  for(let x=-1; x<2; x++){
    for(let y=-1; y<2; y++){
      push();
      translate(x*100, y*100, 0);
      plane(100);
      pop();
    }
  }
}

シェーダを改変して、uTexというテクスチャを用意し、これを出力するようにします。あるいは複数のテクスチャを組み合わせる場合なんかも同様です。デフォルトシェーダはマルチテクスチャを扱えないからです。この場合に、texture()ではなくsetUniformでテクスチャを登録するとどうなるでしょうか。

実行結果:

customCase.png
 はじめの1個しか描画されませんでした。

 もちろん、毎回setUniform()すればいいだけの話です。めでたしめでたし。

wef3f3tg4tg34tg4.png

 しかし、複数あったら複数に対してこれをやるわけです。またforループを使うまでもなく2~3回の場合もあるでしょう。その場合単純に手間は2倍3倍になります。なにより通常描画と結果が異なるのは普通に不自然です。

 ここから先はこの状況を快く思わない人だけ読んでください。今のままでいい人は、お時間取らせてすみませんでした。お引き取りください。

 まず言ってしまうと、2D_ARRAYを使う解決策を提示します。なお、webGLのテクスチャの仕様については
 p5.jsで生のwebglを使ってTextureを作って使ってみる
で死ぬほど詳しく説明したので、それを前提として説明します。ただ、ここで説明してない事情もちょっとだけ使います。

 2D_ARRAYは次のサイトが参考になるかと思います。
 TEXTURE_2D_ARRAY

コード全文

// カスタムケース
// カスタムでuTexというsampler2Dを用意する。
// 残念でした。
// そこで2D_ARRAYです。

/*
  p5の通常のtextureに使うライティングシェーダでは
  2Dの0~2はすべて埋まっている
  厳密には描画前は埋まっていないが
  描画時にドローコール前の段階で埋まってしまいエラーになるようです。
  uSampler
  environmentMapDiffused
  environmentMapSpecular
  これらが0,1,2を占有している
  テクスチャの裏ルールとして
  「同じプログラムは同じスロットから複数のテクスチャを取得できない」
  というのがあるため
  2D_ARRAYは0,1,2を使えない
  じゃあどうするか?

  3を使う。以上。
*/

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

  const gr = createGraphics(100, 100);
  gr.textAlign(CENTER, CENTER);
  gr.textSize(80);
  gr.fill(255);
  gr.text("", 50, 50);

  const sh = baseMaterialShader().modify({
    'fragmentDeclarations':`
      precision mediump sampler2DArray;
      uniform sampler2DArray uTex;
    `,
    'vec4 getFinalColor':`(vec4 color){
      color = texture(uTex, vec3(vTexCoord, 0.0));
      return color;
    }`
  });

  noStroke();
  background(0, 0, 128);

  shader(sh);
  const gl = this._renderer.GL;
  const pg = sh._glProgram;
  gl.useProgram(pg);
  const loc = gl.getUniformLocation(pg, "uTex");
  // 3番に入れる。
  gl.uniform1i(loc, 3);

  const n = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
  console.log(n); // 42.

  // 38,39,40,41がテクスチャ関連。それ以外は割愛する。
  console.log(`SAMPLER_2D:${gl.SAMPLER_2D}`);
  console.log(`SAMPLER_2D_ARRAY:${gl.SAMPLER_2D_ARRAY}`);
  for(let i=38; i<42; i++){
    const unif = gl.getActiveUniform(pg, i);
    console.log(unif);
  }

  // このタイミングではすべてtrueである。つまり、入っていない。
  for(let i=0; i<3; i++){
    gl.activeTexture(gl.TEXTURE0 + i);
    console.log(gl.getParameter(gl.TEXTURE_BINDING_2D) === null);
  }

  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE3);
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);

  const src = gr.elt;
  // 1-5-ゼロの原則、2-6-7の制約、頭はターゲット、おしりはソース
  // に加えてwidth-heightのうしろに1以上の整数を指定するだけ。
  // たとえばここが5の場合ソースはwidth × (height*5) という縦長のサイズでないとだめ
  gl.texImage3D(
    gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, src.width, src.height, 1, // この1を加えるだけ
    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);

  // 一応0に戻しておく
  gl.activeTexture(gl.TEXTURE0);

  for(let x=-1; x<2; x++){
    for(let y=-1; y<2; y++){
      //sh.setUniform("uTex", gr);
      push();
      translate(x*100, y*100, 0);
      plane(100);
      pop();
      // plane()の実行後はすべてfalseが返される。
      // つまり、ダミーが入っている。
      for(let i=0; i<3; i++){
        gl.activeTexture(gl.TEXTURE0 + i);
        console.log(gl.getParameter(gl.TEXTURE_BINDING_2D) === null);
      }
    }
  }
}

実行結果:

wfegfgeg43ef34343f3f.png

プログラムの取得

 まずレンダラーとプログラムを取得します。プログラムがなければはっきり言って何にもできないからです。shの中のプログラムにアクセスするには_glProgramを使うんですが、ここでこれ以降の処理のためにプログラムが起動している必要があります。しかし、

 shader(sh)は実はuseProgram()を実行しない

という仕様になっているため、こっちで無理やり起動させる必要があります。実は最初の描画時に起動するんですが、待ってられないからです(uniformを理解していればわかると思いますが、起動させるのは値をセットするためです。それは描画前でないといけません。そして描画後ではもう手遅れです。起動するのを待ってるわけにはいきません)。

  shader(sh);
  const gl = this._renderer.GL;
  const pg = sh._glProgram;
  gl.useProgram(pg);
  const loc = gl.getUniformLocation(pg, "uTex");
  // 3番に入れる。
  gl.uniform1i(loc, 3);

そんでuTexのロケーションを取得し、3をセットします。つまり使うのは3番スロットです。理由は後で説明します。ここでユニフォームのロケーションって何ですかとか疑問を持った人はこっちを読んでください。
 p5.jsで生のwebglのコードを書いてuniform変数で遊ぶ
説明してる余裕はないです。あと3番スロットって何ですか(以下略)。

予約済みスロット

 ここで天下り的ではありますが、shのuniformの一覧を見てみることにします。これのuniformは42個あるんですが、そのうち38,39,40,41(ラスト4つ)がテクスチャ関連になっています。

  const n = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
  console.log(n); // 42.

  // 38,39,40,41がテクスチャ関連。それ以外は割愛する。
  console.log(`SAMPLER_2D:${gl.SAMPLER_2D}`);
  console.log(`SAMPLER_2D_ARRAY:${gl.SAMPLER_2D_ARRAY}`);
  for(let i=38; i<42; i++){
    const unif = gl.getActiveUniform(pg, i);
    console.log(unif);
  }

  // このタイミングではすべてtrueである。つまり、入っていない。
  for(let i=0; i<3; i++){
    gl.activeTexture(gl.TEXTURE0 + i);
    console.log(gl.getParameter(gl.TEXTURE_BINDING_2D) === null);
  }

SAMPLER_2D:35678
SAMPLER_2D_ARRAY:36289
WebGLActiveInfo {size: 1, type: 35678, name: "environmentMapDiffused", constructor: Object}
WebGLActiveInfo {size: 1, type: 35678, name: "environmentMapSpecular", constructor: Object}
WebGLActiveInfo {size: 1, type: 35678, name: "uSampler", constructor: Object}
WebGLActiveInfo {size: 1, type: 36289, name: "uTex", constructor: Object}

 なお、シェーダーの方で先に2D_ARRAYを指定しました。これも説明は後回しにします。uTexは2D_ARRAYということです。その上の3つです。これらのテクスチャスロットでの位置は0,1,2となっています。というかこの時点では入ってないんですが、それはそのあとのforループで確かめられます。すべてtrueです。つまりnullです。
 ですが、実はplane()の実行後はすべてfalseになります。何にもしてないのに...?そうです。で、ここでテクスチャの裏ルールが登場します。

texture衝突.png

 実は、どの部屋にも複数の種類のテクスチャは共存できるんです、それは正しいんですが、制約があって、

「同じプログラムは同じスロットから複数のテクスチャを取得できない」

んです。つまり0番スロットから2DとCUBEを取得するとかそういうことができないんですね。それで2D_ARRAYを今回使いたいので、3番を使っているわけです。

 ところで、違う部屋なら2Dでもいいじゃないかと思うかもしれないんですが、だめです。理由は正直よく分かりませんでしたが、2Dで3番に入れてやったら失敗しました。要するに、2D_ARRAYは現在p5によりサポートされてないので、抜け道を使おうというわけです。まあ動けば何でもいいです。

 trueがfalseになるのは実は描画時のドローコールの寸前のタイミングです。つまりplane()の実行時に衝突が起きて失敗するわけです。なので、あらかじめ部屋を避けておく必要があります。

2D_ARRAYの使い方(ざっくり)

 とりあえずざっくりと2D_ARRAY, まあさっきの記事を読めばいいんですが、動けばいいという人のためのガイドです。
 まず3番に今回入れるので、3番を活性化してバインドします。ターゲットを2D_ARRAYにします。

  const tex = gl.createTexture();
  // 3番に入れる
  gl.activeTexture(gl.TEXTURE3);
  gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);

画像の導入にはtexImage3Dを使います。ついでにフィルターも設定しましょう。

  const src = gr.elt;
  // 1-5-ゼロの原則、2-6-7の制約、頭はターゲット、おしりはソース
  // に加えてwidth-heightのうしろに1以上の整数を指定するだけ。
  // たとえばここが5の場合ソースはwidth × (height*5) という縦長のサイズでないとだめ
  gl.texImage3D(
    gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, src.width, src.height, 1, // この1を加えるだけ
    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);

 texImage3Dで2D_ARRAYを設定する際の注意です。まず、内容的にはtexImage2Dとほぼ一緒です。パラメータ設定ですが、自分が教えた「頭はターゲットでおしりはソース」「1-5-ゼロの原則」「2-6-7の制約」「残った3-4に横と縦」でパラメータを並べればほぼ完成です。最後にwidth-heightの後ろのところに1つだけ数を追加します。これは入れる場所を考えればわかりますが、要するに「厚さ」です。

 2D_ARRAYの場合、たとえばここが50だとすると50枚のテクスチャをまとめて扱う形になります。実はwidth-heightは2D_ARRAYの場合各々のテクスチャの横と縦で、たとえばあそこが50の場合、それが50枚となります。なのでグラフィックデータとしては横がwidthで縦が50xheightとかなります。縦長というわけですね。フロートとかだとまた違った感じになるかもしれないですね。今回は1枚しか使わないので、1を指定します。そうなるとソースはそのまま入れればいいですね。

 glslサイドの注意です。まずあのサイトにもある通り、sampler2DArrayはprecision設定が必須で、無いとエラーを食らいます。

fs
  precision mediump sampler2DArray;

また、各々のレイヤーには、引数がvec3になるんですが、これのz座標のround値でアクセスするようです。今の場合普通に0でいいですね。

fs
  color = texture(uTex, vec3(vTexCoord, 0.0));

 なおフィルター設定は2D_ARRAYの場合すべて共通です。2D_ARRAY, 使いこなせると便利かもしれないですね。数の画像やキャラクター画像をまとめて扱いたい場合に非常に重宝しそうです。

描画

 準備が終わりました。描画が実行されました。クリーンアップされません。全部描画されました。めでたし、めでたしです。なお、このタイミングで0,1,2のTEXTURE_2Dになんか入ったようです。

 知ったこっちゃないですね。

おわりに

 こんなのほとんどのユーザーは気にしないでしょう。どうでもいいかもしれません。しかし2D_ARRAYを知るいいきっかけにはなりました。人生何が起きるかわからないものなので、気になったらどうでもいいと一蹴せず、気にしてみることが大事なように感じました。

 ここまでお読みいただいてありがとうございました。

 今回取り上げた問題が議論されているissue:
 Image uniforms get reset after each draw using a shader #7030

余談:基礎

 基礎って大事ですね。何が大事って言えばこの記事ですよね。だって、基礎を前提にできれば、

 面倒な説明の大部分を省略できる

んだから。いちいちuniformやtextureの仕組みを一から説明しなくていいからめっちゃ楽。基礎、いいですね~。ていうかテクスチャを例に取り上げるけれど、全部同じ仕組みなんですよね。スロットがあって、バインド方法があって、フィルターやラップの設定がある。この基礎的な部分さえきちんと理解しておけば、新しい概念を覚えるにも差分だけ覚えれば済むわけです。さっきのtexImageの引数設定にしてもね。だから基礎は本当に大事だと思います。

ちょっと長い補足

 さてこうして問題は解決したんですが、実は肝心なことを何にも説明できていません。そもそもなぜ通常のtexture()を使う場合は問題ないのか?カスタムシェーダと言うがカスタムかどうかを本当に判定しているのか?そしてなぜ2D_ARRAYだとリセットされないのか?すべて説明していません。なぜかうまくいっただけ、そんな感じですね。まあ動けばいいの観点からすればそれでいいんですが、完全に片手落ちなので、ちょっとだけ補足します。

 そうですね、結論から言うと...

  • texture()で用意したテクスチャは特別扱いされる。故に描画のたびに適用される。ただし定員一名
  • 実はすべてのユニフォームはすべてのシェーダにおいて描画のたびに唯一のゴミを指定するように書き換えられている。そこにデフォルトとカスタムの区別はない。サンプラを扱ってるならすべて
  • ただし、p5が把握できるのはTEXTURE_2Dのみである。他のタイプのテクスチャが存在していてもその存在を把握できない。故にリセットもできない

ざっくり言うとこんな感じです。詳しく説明します。ここからは自分が軽くハッキングした結果得られたいくつかの事実を羅列するものとします。

なぜ通常のtexture()を使うとセーフなのか

 まず当たり前ですが、textureの引数は一つしかありません。そもそも複数のテクスチャであれこれやろうとすれば必然的にカスタムシェーダになります。デフォルトである限り、一つしかテクスチャは扱えないです。それは当然として、この関数でテクスチャを登録する場合何が起きるかというと、まずp5レンダラーの_texプロパティにソースがダイレクトにぶち込まれます。

texture
  this._renderer._tex = tex;

 それで、ここに登録されたテクスチャというのは、描画のたびにsetUniformが実行される仕組みになっています。

_setFillUniform
  if (this._tex) {
    fillShader.setUniform('uSampler', this._tex);
  }

これだけです。というかまあ一つしか管理できないですからね。で、setUniformは重複をキャッシュにより防いでいることを念頭においてください。同じものが来たら失敗するわけですね。たとえば一番最初の描画の場合キャッシュは空なので、当然実行されます。実はその時にこのソースからtextureObjectが生成され、それはMapという構造によりソースをキーとして登録されます。で、それが然るべき場所にバインドされます。一番最初のサンプルの場合、0,1,2が使われるんですが、場所にして2の2D枠にバインドされます。で、描画されます。使われました。そのあとでunbindTexturesを実行しています。

 unbindTexturesとは、p5が管理しているプログラム内のサンプラーユニフォームに対して、たった一つのちっこいテクスチャ(ゴミ)を対応させる処理です。どんなゴミかというと:

  // a plain white texture RGBA, full alpha, single pixel.
  var im = new _main.default.Image(1, 1);
  im.set(0, 0, 255);
  this._emptyTexture = new _main.default.Texture(this, im);

これですね。実は説明していませんでしたが、

 テクスチャの裏ルール:同じテクスチャは同じターゲットである限り複数のスロットに登録できる

というのがあって、これを使って描画のたびにsetUniformですべてのサンプラーユニフォームをゴミにしているわけです。このゴミはただ一度だけ生成され、登録されます。

 ゴミが登録されている状態で、_texの特別扱いでソースを指定した場合、ソースとゴミは異なるので、きちんとsetUniformが描画のたびに実行されます。そういうわけで、ただ一つだけ、uSamplerだけ、描画のたびに使われるんですね。マルチなんかやるなというわけですね。textureで登録されていなければ、ゴミを割り当てられて、そのままです。最初だけは登録処理が行われますが、それ以降は_texに登録されたソースのように特別扱いしてもらえないので、ずっとそのままだし、ゴミを割り当てられてずっとそのままです。setUniformにより動的に指定しない限り、は。

 ゆえにカスタムシェーダとsetUniformの場合、texture()が使用されないので、すべてのサンプラオブジェクトは毎回ゴミにされるわけです。もちろんtexture()を使えばuSamplerだけはゴミになるのを回避できます。つまりカスタムかデフォルトかは関係なくて、単にゴミにされるのを回避できるテクスチャがuSamplerしかないんですよね。そしてそれを実行するにはtexture()を実行するしかないと。そういうわけです。

なぜ2D_ARRAYだとリセットされないのか

 じゃあなぜ2D_ARRAYのサンプラユニフォームはゴミにされるのを回避できるかというと、p5がそれを把握できないからですね。

_loadUniforms
  if (uniform.type === gl.SAMPLER_2D) {
    uniform.samplerIndex = samplerIndex;
    samplerIndex++;
    this.samplers.push(uniform);
  }

 これはシェーダーのユニフォーム情報を登録するコードの一部です。こんな感じで、ユニフォームの型がSAMPLER_2Dの場合しか把握できない仕様なんです。それでsamplersに登録されます。このsamplersの中身がゴミ化の対象です。つまり2D_ARRAYで作るとゴミ化の対象から外れるので、ゴミで埋められないわけです。ゆえに描画のたびにリセットされません。そのかわり登録もできないので、プログラムを使って手動でやるしかありません。完全に自由です。自由と責任です。そういうわけですね。

そもそもなぜこのような仕様にしたのか

 ここまでは良いんですが、70点ですね...事の発端はpagurekの自作フレームバッファのバグ対応です。
 Unbind textures when done with a shader #5923
次のエラーを解消するためのものです。

 GL_INVALID_OPERATION: Feedback loop formed between Framebuffer and active Texture.

フレームバッファの初歩でやりがちなミスです。自分もやらかしました。これを解消するために、先ほどのゴミ対応を導入したのがpagurekです。この辺は自分も何となくうまくいって、それっきりほったらかしできてしまったので人のことは言えないですね。ただ一つ言えることは、自分のライブラリではこんな〇〇〇たテクスチャ管理はしてない、ということだけです。勉強不足なので、いずれ補足したいと思います。

 基礎は大事ですね。自分のライブラリではサクサクフレームバッファを扱えるんですが、それは大変結構なことなんですが、こんなバグの原因も分かんないようじゃだめですね。もっとも、普通に扱っていれば起きないはずなんですよね...自分がこれと最初に出会ったのはマルチレンダーターゲットの時なので。通常の運用でこれに出くわすのはよほど雑な実装でもしてない限り...

p5のライティングフィル描画は常にテクスチャ描画を想定している

 これがどうも悪さをしてるっぽいです。pagurekのバグコード:
 バグコード
を解析してたんですが、途中であきらめました。なので途中までです。

 まず、最初のtexture(fbo.color)で読み込み先としてfbo.colorに相当するテクスチャが用意されます。その次のfbo描画では、それが読み込まれる前提で描画が実行されます。ライティングフィルシェーダーは常にテクスチャ描画を想定するからです。実行すると分かりますが、lights();をnoLights()にすると問題なく描画が実行されます。テクスチャを使わないからです。

wdfwfgwfgev.png

 テクスチャを使う場合、書き込み先もfbo.color, 読み込まれるのもfbo.colorで衝突が発生します。fboはざっくりいうと「テクスチャにドローコールで描き込みを実行するためのインタフェース」です。バッファと名前が付いていますが、

 フレームバッファはバッファではありません。ただのインタフェースです。

 TFFでwebGLバッファに描き込めるわけですが、webGLバッファに描き込むのがTFFならテクスチャに描き込むのがFBOというわけですね。いずれも、逆はできません。役割分担です。で、その描き込む先のテクスチャは読み込みに使えないので、エラーになるわけです...多分。

 それをこれで回避しています。

  this._renderer._curShader.setUniform("uSampler", this._renderer._getEmptyTexture());

 これを一番最後の、drawの最後の行として追加すると動きます。これが今現在の解決策ですが、何でこれで動くのかはsetUniformの挙動が複雑すぎて手に負えませんでした。お手上げです。本来uniform1234fivでなんかプログラムにセットする「だけ」の分かりやすい関数なんですが、ライブラリに埋め込まれるともうわけがわからなくなってしまうんですよね...さすがにお手上げです。

 この「ライティングフィルシェーダーが常に四六時中テクスチャ描画を想定して挙動する」というのはいろんな問題を引き起こしていて、たとえば自分が引っかかったスマホ限定の挙動依存バグもこれが原因だったりするのを以前話したかもしれません(そういうのを回避するのがVAOですが、もちろん導入されていません)。というか本来テクスチャはユーザーがすべて管理下に置くのが本来のあるべき姿だと思います。使う場合も、何を使うのか明確にユーザーが決められれば衝突など起きようがないです。このコードの描画では単色ライティングをしています。テクスチャを使いません。本来衝突は起こりえないはずです。テクスチャを使わないシェーダを使えば普通にライティングは可能なのに横着をしているわけです。p5は保守管理が面倒といった理由で同じシェーダーに2つも3つも役割を兼用させているからです。その弊害が出ているわけですね。

 (ちなみに自分のライブラリではライティング用のシェーダーを作る際に、初期化時にテクスチャや頂点彩色を使うか否かを選べるようになっています。そしてもちろんテクスチャを使う場合であってもそれらは必要十分なだけ用意され、使う場合にはきちんと何かしら入れるので、ゴミかなんかでごまかす必要もないです。とても快適です。)

 空しいですね。
 あのスマホのバグが起きた時は悲しかったですね。パソコンでは普通に見られるのに、スマホだと消えているわけです。スマホで見るなということでしょうか。誰にそれを決める権利があるのか。
 引っかからない人にはどうでもいい話ですね。

 また、このバグが引き起こされるのはpagurekのfboを使う場合だけです。当たり前ですが...普通の描画ではテクスチャに対してドローコールで描き込みを実行するのは不可能だからですね。しかしこの対応の影響はfboを使わないユーザーにも及んでいます。それについてとやかく言うつもりはありません。代わりに簡単なfboを用いた描画の作例を置いて、この記事の締めとします。本来こういうものです。デプスも、ステンシルもアタッチしてないので、初歩の初歩といった扱いのものですが、雰囲気はつかめるかと思います。

きわめて初歩的な、初歩のフレームバッファの取り扱い

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

  const gl = this._renderer.GL;

  const tex = gl.createTexture();
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 400, 400, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

  const fbo = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
  gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  
  const vsITA =
  `#version 300 es
  const vec2 LEFT_DOWN = vec2(-1.0, -1.0);
  const vec2 RIGHT_DOWN = vec2(1.0, -1.0);
  const vec2 LEFT_UP = vec2(-1.0, 1.0);
  const vec2 RIGHT_UP = vec2(1.0, 1.0);
  out vec2 vUv;
  void main(){
    int i = gl_VertexID;
    vec2 p;
    if(i==0){ p=LEFT_DOWN; }
    if(i==1){ p=RIGHT_DOWN; }
    if(i==2){ p=LEFT_UP; }
    if(i==3){ p=RIGHT_UP; }
    vUv = p*0.5 + 0.5;
    vUv.y = 1.0 - vUv.y;
    gl_Position = vec4(p, 0.0, 1.0);
  }
  `;

  // 正しければ(0,0)は左上なので左上は青くなるはず。
  const fsITA =
  `#version 300 es
  precision highp float;
  in vec2 vUv;
  out vec4 fragColor;
  void main(){
    vec3 color = vec3(vec2(vUv.y), 1.0);
    fragColor = vec4(color, 1.0);
  }
  `;

  // yだけ逆にする
  const fsITA2 =
  `#version 300 es
  precision highp float;
  in vec2 vUv;
  uniform sampler2D uTex;
  out vec4 fragColor;
  void main(){
    vec2 uv = vUv;
    uv.y = 1.0 - vUv.y;
    fragColor = texture(uTex, uv);
  }
  `;

  const pgITA = createShaderProgram(gl, {
    vs:vsITA, fs:fsITA
  });
  const pgITA2 = createShaderProgram(gl, {
    vs:vsITA, fs:fsITA2
  });

  gl.useProgram(pgITA);

  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);

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

  gl.useProgram(pgITA2);
  setUniformValue(gl, pgITA2, "1i", "uTex", 0);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

  gl.flush();
}

function createShaderProgram(gl, params = {}){
  /* ここから先はいつもの...たとえばテクスチャの記事で紹介したのと同じデフォルト設定 */

ビューポートの説明などいろいろ端折っていますがこんな感じです。テクスチャを紐付けると、ドローコールの対象がテクスチャになるわけです。後半ではそれを読み込んで描画を実行しています。

blue.png

テクスチャの設定でソースにnullを指定していますね。TFFでやったと思いますが、バッファサイズだけの領域を用意したと思います。あれと同じことをしています。400,400で640000バイトの空っぽの領域を確保してそこに描画しているわけです。wgldの記事:
 フレームバッファ
ではフレームバッファがまずあって、そこにテクスチャをフェッチしないといけなくって、さらにそれを再利用できるようにしないといけない的なことが書いてありますが、むしろ逆で、最初にテクスチャがあって、そこに描き込みたいからフレームバッファを用意するんです。もっとも真に受けた自分が悪いんですが......

なお、webgpuではテクスチャへのレンダリングが直接実行できるようになったため、フレームバッファの概念は存在しないようです...参考:テクスチャにレンダリングする

 もっとも、これから付き合っていくのはp5のfboでしょうから、去年末のアドベントカレンダーで紹介された、次の記事を置いておきます。
 p5.jsのFramebufferを使ってみる

泥舟

 基礎は大事です。基礎が無いと迷ったときに頼りとできるものが無いので、その上になんか積み重ねようとしてもグラグラしておぼつかなくなってしまいます。
 pagurekのことをあんま悪く言うつもりはないんですが、2022年の12月に携わり始めて以降、あの人が関連するバグを折に触れ見てきたんですよね。特にベジエの、無限にエラーが出るバグ、1.5.0のですが、あれはひどかったですね。ああいう一時しのぎが多すぎるんですよね。カメラの800とかもそうなんですが。しかも他の人がそれをやるのも容認してたり。結局結果がすべてで、見かけ上バグが消えたなって思ったらそれでOKにしちゃうところがあるのは気に入らないです。fboにしてもそうですが、基本的にあの人は、自分に対する不利益が無くなったかどうかがバグ対応の基準なので、周りは目に入ってないですね。p5のwebGLはほぼ完全にあの人が牛耳ってる状態なので、何をしても受け入れられてる印象があります。自分はもうどうでもいいんですが、今後のことを考えると割と不安だなと思ったのでした。個人の感想です。

 これとかひどかったですね。

 p5.Camera.slerp() behaves strangely #6508

もう疲れたんですよね。3Dも、CGも、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?