はじめに
フレームバッファに深度バッファをアタッチする復習をしたいと思いました。
まず、フレームバッファはテクスチャに描画を実行するためのインターフェースです。
デフォルトフレームバッファという概念があって、要するにキャンバスに描画する際の枠組みです。デフォルトの状態では色バッファがキャンバスを元にしたテクスチャで用意されてるっぽい(あんま詳しくない)です。それで深度値とステンシルについては、それも用意されているんですが、デフォルトでは機能していません。enableする必要があります。p5の場合、深度値はデフォルトで有効になっています。ステンシルは有効になっていません。
それで、デフォルトではないフレームバッファ、つまりこっちが用意したフレームバッファの場合、それは何にも用意されていないので、こっちで用意する必要があります。用意するのは色と、深度値と、ステンシル値です。これらのアタッチメントに、テクスチャ、もしくはレンダーバッファをはめる必要があります。最も色に関しては常にテクスチャでしょう。レンダーバッファの場合、書き込まれるだけで再利用できないからです。今回は深度値をテクスチャとレンダーバッファの両方で用意したいと思います。サンプルに使うのは以前用意した3D描画の枠組みです。
簡単な法線彩色です。
描画の枠組みですが、p5でやってもいいんですが、処理がいろいろと隠蔽されていて面倒なので、こっちの機構でやろうと思います。そこは本質的でないんで、問題ないですね。
コード全文
もう300万スケッチこえたんですね...
// fork: https://openprocessing.org/@u161812/2755983
const {Render2D} = fisce.foxApplications;
const {ProgramWrapper, VAOWrapper} = fisce.webglUtils;
const vs =
`#version 300 es
in vec3 aPosition;
in vec3 aNormal;
uniform mat3 uModel;
uniform vec3 uEye;
uniform vec3 uViewX;
uniform vec3 uViewY;
uniform vec3 uViewZ;
uniform vec4 uProj; // fov,aspect,near,far
out vec3 vNormal;
void main(){
vNormal = aNormal;
vec3 p = aPosition * uModel;
p *= 90.0;
p -= uEye;
vec3 q = vec3(dot(p, uViewX), dot(p, uViewY), dot(p, uViewZ));
float fov = uProj.x;
float aspect = uProj.y;
float near = uProj.z;
float far = uProj.w;
float factor = 1.0/tan(fov/2.0);
gl_Position = vec4(
q.x * factor / aspect,
q.y * factor,
((near+far)/(near-far)) * q.z + (2.0*near*far)/(near-far),
-q.z
);
}
`;
const fs =
`#version 300 es
precision highp float;
in vec3 vNormal;
out vec4 fragColor;
void main(){
fragColor = vec4(0.5+0.5*vNormal, 1.0);
}
`;
function setup() {
createCanvas(320,480,WEBGL);
smooth();
pixelDensity(1);
const gl = drawingContext;
noStroke();
torus(1,0.4,64,16);
clear();
const vao = VAOWrapper.scan(gl);
const eye = createVector(0,0,240*sqrt(3));
const center = createVector(0,0,0);
const near = 20*sqrt(3);
const far = 2000*sqrt(3);
const aspect = width/height;
const fov = PI/3;
const v0 = center.copy().sub(eye).normalize(); // 視線方向単位ベクトル
const up = createVector(0,1,0); // 暫定上方向ベクトル
const viewX = v0.cross(up).normalize();
const viewZ = v0.copy().mult(-1).normalize();
const viewY = viewZ.cross(viewX).normalize();
const pg = ProgramWrapper.build(gl, {vs, fs});
gl.enable(gl.CULL_FACE);
const texColor = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texColor);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 320, 480, 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);
// depthのテクスチャ
const texDepth = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texDepth);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, 320, 480, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// depthのレンダーバッファ
const renderDepth = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderDepth);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT32F, 320, 480);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texColor, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texDepth, 0);
//gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderDepth);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
const r2d = new Render2D(gl);
r2d.build(`
@fs
<uniform> sampler2D uTex;
<main> color = texture(uTex, vec2(uv.x, 1.0-uv.y));
`);
draw=()=>{
pg.use();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
const t = frameCount*TAU/240;
background(0);
const axis = createVector(1,1,1).normalize();
pg.setUniform("uModel", [
cos(t) + (1-cos(t))*axis.x*axis.x, (1-cos(t))*axis.x*axis.y - sin(t)*axis.z, (1-cos(t))*axis.z*axis.x +sin(t)*axis.y,
(1-cos(t))*axis.x*axis.y + sin(t)*axis.z, cos(t) + (1-cos(t))*axis.y*axis.y, (1-cos(t))*axis.y*axis.z - sin(t)*axis.x,
(1-cos(t))*axis.z*axis.x - sin(t)*axis.y, (1-cos(t))*axis.y*axis.z + sin(t)*axis.x, cos(t) + (1-cos(t))*axis.z*axis.z
]);
pg.setUniform("uEye", [eye.x, eye.y, eye.z]);
pg.setUniform("uViewX", [viewX.x, viewX.y, viewX.z]);
pg.setUniform("uViewY", [viewY.x, viewY.y, viewY.z]);
pg.setUniform("uViewZ", [viewZ.x, viewZ.y, viewZ.z]);
pg.setUniform("uProj", [fov, aspect, near, far]);
vao.bind();
vao.drawElements('triangles');
vao.unbind();
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);
r2d.useProgram();
r2d.setUniform('uTex',0);
r2d.render();
r2d.clearProgram();
gl.flush();
}
}
色バッファにアタッチするテクスチャ
こんな感じで用意します。
const texColor = gl.createTexture();
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texColor);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 320, 480, 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);
0番に置きましょう。nullとあるのは入れ物だけ用意したいからです。あとから書き込まれるんで、中身は必要ないです。
深度バッファにテクスチャをアタッチする場合
その場合は深度値もテクスチャで用意します。フォーマットはいくつかあるんですが、今回は深度値に特化した32bitフロートのやつでやりましょう。1番に入れておきます。
// depthのテクスチャ
const texDepth = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texDepth);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, 320, 480, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
これで準備ができたので、インタフェースを用意してアタッチしましょう。
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texColor, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texDepth, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
色のアタッチメントに0番のやつをアタッチして深度のアタッチメントに1番のやつをアタッチします。ステンシルはやりませんが注意だけしておきます。ステンシルを使う場合、深度とステンシルは同じテクスチャ、もしくはレンダーバッファでないとエラーになるようです。その場合、通常は24-8か32-8のデプスステンシル(通称デプステ)を使うことになります。ちなみにデフォルトフレームバッファは24-8のデプステを使ってるっぽいです。
描画して表示してみる
今回サイズが一緒なのでビューポートはいじらなくてOKです。最初にバインドした後で描画して、そのあとでバインドを切っています。そのあとの処理ですが、これにより0番テクスチャに色が書き込まれるので、それを2Dのレンダリング機構で表示しています。自前の機構でやっていますが、uvのyだけ逆にしています。なぜかというと、テクスチャへの描画はビューポート座標を元にして実行されるため、左下に相当するピクセルが(0,0)になってしまうんですね。なのでそれが左上に来るよう、こっちの機構は左上が(0,0)になる感じで調整してあるんで(通常のテクスチャ描画に即している)、逆にしてフェッチしています。
レンダーバッファを使う場合
それではお待ちかね、レンダーバッファを使う場合です。テクスチャと違ってGPUサイドで描画するだけなので、内容的にはシンプルです。内部フォーマット(例によって理解が不十分)とサイズを指定して終わりです。
// depthのレンダーバッファ
const renderDepth = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, renderDepth);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT32F, 320, 480);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
こっちも高精度の32bitFloatにしています。アタッチするには、こう:
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texColor, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, renderDepth);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
framebufferRenderbufferとかいう投げやりな名前の関数を使います。0と2番目は確定なので実質引数は2つしかありません。1番のどこにアタッチするか、それと、3番のアタッチするレンダーバッファです。自分が関数化するならここしか指定させないですね。
面倒だし。
それで、結果はどうなるかというと、一緒です。違いはと言えば前者の場合テクスチャを再利用できるんですが、後者はできません。そこだけですね。
深度テクスチャを見てみよう
深度をテクスチャで用意したのなら、同じ方法で内容を見ることができるでしょ?って思うわけです。できます。しかしちょっと注意が必要なようです。ここ:
gl.clearColor(1,1,1,1);
gl.clear(gl.COLOR_BUFFER_BIT);
r2d.useProgram();
r2d.setUniform('uTex',0); // ここ
r2d.render();
r2d.clearProgram();
gl.flush();
ここの0を1にするだけですが、どうやら深度テクスチャを描画に使う場合、フィルタリングで問題が発生するようです。上でLINEARにしたと思いますが、あそこの設定はそもそもフェッチに使う情報なので、フェッチしないなら不要です。それでフェッチすることになったのでこの問題が発生するんですが、実はLINEARだと駄目なようです:
WebGL warning: drawArraysInstanced: TEXTURE_2D at unit 1 is incomplete: Minification or magnification filtering is not NEAREST or NEAREST_MIPMAP_NEAREST, and the texture's format is not "texture-filterable".
ざっくりいうとNEARESTにしろってことです。そういうわけでNEARESTにします。
// depthのテクスチャ
const texDepth = gl.createTexture();
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texDepth);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.DEPTH_COMPONENT32F, 320, 480, 0, gl.DEPTH_COMPONENT, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
これで表示されます。なぜ真っ赤かというとREDしか機能しておらず、かつ描画先が不透明で、かつp5の恩恵によりデフォルトでブレンド機構が働いているからです。
ただDEPTHをテクスチャで用意することはおそらくほとんどないので、あんま気にする必要は無いかもしれないです。
おわりに
いい復習になりました。ここまでお読みいただいてありがとうございました。
いくつかの補足
補足
使ってる関数について
こっちの機構でいくつかの関数を使っています。p5と絡めて使うのに便利ですが、こっちのp5を使わないコードでも活躍していてとても使いやすいです。
VAOWrapper.scan
そのときのVAA(vertex attribute array)及びバインドされている可能性のあるインデックスバッファの状態から、それをそっくりそのまま復元できるVAOを作り出す関数です。バッファについてはすべてコピーです。ただしdisableなものは省きます。p5はVAOを使ってないせいで常に描画後にその描画のために準備したVAAの状態がほったらかしになる仕様なので、簡単にパクることができます。もっとも描画方式がこっちと向こうで一致してないといけないという制約があり、その制約のせいでstroke描画については復元できません。まああんな重いもの描きたくないのでどうでもいいです。ちなみにこの64,16のトーラスのstroke描画のVAOですが...
64,16ではトーラスのstrokeを描画できない
らしいので無理です。なんだそりゃ。それでXは24が限界、Yは16が限界ですが、その24,16で頂点数を見ると...18924だそうです。ありえないですね。24と16でそれなのか...正直あのstroke描画が何のためにあるのか、いまだによく分かりません。
ProgramWrapper.build
サクッとvs,fsからプログラムを作る関数です。作るだけならこれでいいんですが、UBOやTFFをやりたい場合に必要なセットアップを実行するための機能も備えています。自分のライブラリのモットーは、
基本的なことはサクッと
応用的なこともそれほど苦労せずサクッと
なので、楽なことはとことん楽にできるように作っています。返るのはプログラムではなくProgramWrapperなので使うにはuseProgramではなくuseを使います。まあこれ自身がプログラムなので、わざわざProgramとか書く必要は無いですね...それで、setUniformも同じようにできるんですが、p5のと違って行列やベクトルをそのまんまぶち込めます。何なら構造体も扱えます。万能です。
Render2D
WebGLの2D描画用の機構です。2D描画というより板ポリ芸用です。要するに画面全体を覆うやつでなんかしようというわけです。fsでuv使ってあれこれできます。uvのデフォルトは左上(0,0)の右下(1,1)ですがこれをいじったりできます。描画のための位置情報もvsでpositionをいじると実行できたりします。
仕様が固まったらレファレンスとか作りたいですね...今はちょっと忙しくて無理ですね。
デプステクスチャに深度値を落とすだけの使い方をしたい場合
深度バッファにテクスチャをアタッチしてその中身を見たと思うんですが、もし深度バッファに深度値を落としたいだけで描画はどうでもいい場合、色バッファへのアタッチは不要なんじゃないか?という疑問がわいてきます(わいてきてくれ)。実は、不要です。それを見ます。
まず、単純にフラグメントシェーダで色出力の機構をなくしてみます。不要なバリイングも切ってしまいましょう。TFFのような感じですね。
#version 300 es
void main(){}
なおプレシジョンはバリイングが残ってると排除できないので気を付けてください。書きませんがvsからもバリイング関連の記述をなくしてあります。この状態で、普通に描画すると、エラーを食らいます:
WebGL warning: drawElementsInstanced: Program has no frag output at location 0, the destination draw buffer has an attached image, and its color write mask is not all false, and DRAW_BUFFER0 is not NONE.
ざっくりいうと「色バッファにアタッチしてるんだからアウトプット用意しろや!用意できないんだったら色マスクで全部falseにするか、色バッファに書き込まれないように処理しておけや!」です。描画不履行エラーを食らうので、何も表示されない...のではなく、真っ赤になります。だってデプスクリアでオール1になってるからね。
そこで、色へのアタッチをやめてしまおう。
// 深度バッファにだけアタッチする
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texDepth, 0);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
色バッファをアタッチしたままこのシェーダーで実現したい場合、やり方が2つあります。
1. drawBuffersで色バッファへの書き込みをやめさせる
この若干分かりにくい関数を使います。なぜか配列で指定するようです...おそらく、指定した順に処理を実行していくのかもしれないです。わかんないけど。たとえばNONE,COLOR_1とすることで1だけ有効になったりするのでしょう。なおバインドされているフレームバッファに依存する仕様のため、バインド内で使っても外に影響は出ません。
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texColor, 0);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.TEXTURE_2D, texDepth, 0);
gl.drawBuffers([gl.NONE]);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
こうすることで色バッファへの書き込みが為されなくなり、アウトプットを省略できるようになります。なお戻すにはこうします。
gl.drawBuffers([gl.COLOR_ATTACHMENT0]);
おそらくレファレンスにある方法というのは、まずNONEですべて描画不可とし、そのあとで1番だけ書き込み可能にする、とかそんな感じなんじゃないですか?いずれMRTやるときお世話になるかもしれないです。場合によっては色バッファにも書き込みたい場合に切り替えるにはこうするわけです。
2. 色マスクを切る
もうひとつ、色マスクを切る方法があります。
gl.colorMask(false,false,false,false);
全部切れば、色バッファは無視されるので、描画できない仕組みでも問題ありません。この方法の注意点は、drawBuffersとは違い、グローバル状態をいじる点です。つまりフレームバッファに依存しないので、これをバインド内で切ろうが、バインド外で切ろうが、色バッファに書き込む際、これは適用されます。そのときバインドされているフレームバッファ(もしくはデフォルトフレームバッファ)の色バッファへの書き込みがすべて禁止されるので、そのままだと深度テクスチャの描画もできません。ちゃんと戻しましょう。
gl.colorMask(true,true,true,true);
色バッファを使う場合は、このように随時切り替えることで対処できます。
ブレンドについて
p5のブレンドのおかげで真っ赤になる、とか適当なことを書いたと思うんですが、おそらく違っていて、どうやらテクスチャのalphaには1が入っているようです。ここを...
<main> color = texture(uTex, vec2(uv.x, 1.0-uv.y)).aaaa;
にすると真っ白になります。つまりalphaは1ということです。なぜそうなのかは分かりませんがそうなっているようです。だから普通の不透明の赤(もしくは赤っぽい色)で、ブレンド関係なかったですね。
補足は以上です。


