はじめに
p5.jsは日々進化しています。今ではframebufferやshaderのカスタマイズを実行することができます。そしてframebufferのformatはなんと、浮動小数点数をサポートしています。これを使わないのはもったいないので、今回はこれを利用して、shaderで浮動小数点数テクスチャを扱ってみたいと思います。これを読むにあたっては、毎度お世話になっています、こちらのサイトが参考になるかと思います。
浮動小数点数VTF
「浮動小数点数テクスチャ」で検索したら影描画がヒットしてしまったので、それは今回の趣旨と外れてしまうので採用せず、とにかくバーテックスシェーダで浮動小数点数が格納されたテクスチャを利用する、あくまでそういう趣旨のプログラムをここでは扱うこととします。
内容的には、負の数や1を超える数ですね、これをテクスチャに格納し、バーテックスシェーダで利用します。アトリビュートの値を変更し、描画に反映させます。具体的には-1と2です。色しか扱えない場合、自動的にクランプされてしまいこのような値は扱えないのです。つまり、浮動小数点数として取り扱うことでしかできません。
なお、その際点描画を使わなければならないのですが、そこまで難しいことはやっていないので大丈夫です。多分...
コード全文
// VTFできましたね
// なんだ~
// float textureってこれでいいんですね~
// まああんま使い道ないだろうけどな
let fbo;
let sh;
let mainSh;
let buf1;
const vert =
`#version 300 es
in vec4 aData;
out vec4 vData;
void main(){
vData = aData;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
`;
const frag =
`#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
color = vData;
}
`;
function setup() {
createCanvas(600, 600, WEBGL);
const _gl = this._renderer;
const gl = _gl.GL;
buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,1,2,-2]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
sh = createShader(vert, frag);
fbo = createFramebuffer({
width:1, height:1, format:FLOAT, textureFiltering:NEAREST
});
fbo.begin();
shader(sh);
_gl._setPointUniforms(sh);
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
sh.enableAttrib(sh.attributes.aData, 4);
gl.drawArrays(gl.POINTS, 0, 1);
sh.unbindShader();
fbo.end();
mainSh = baseMaterialShader().modify({
'vertexDeclarations':`
uniform sampler2D uData;
`,
'vec3 getLocalPosition':`(vec3 p){
vec4 v = texture(uData, vec2(0.5, 0.5));
p.x += v.x; // x座標を-1
p.y += v.z; // y座標を+2
return p;
}
`
});
}
function draw() {
background(220);
orbitControl();
shader(mainSh);
mainSh.setUniform("uData", fbo.color);
noStroke();
lights();
fill(0,128,255);
box(100);
}
box()関数の頂点アトリビュートに干渉し、x座標を-1したうえでy座標を+2します。そうすると描画位置がずれてこうなります。
vが格納した4つの浮動小数点数からなるベクトルで、x座標に-1、z座標に2が入っているわけです。実際そうなっていることはこれを見ればわかります:
-1.0と2.0になっているのが分かるでしょうか。こういう値になっていれば成功というわけですね。
framebufferの準備
詳細はこちらを参照:createFramebuffer
(独楽回しEddyさんがframebufferに関する記事を書いてくださいました。こちらも参考になるかもしれません:p5.jsのFramebufferを使ってみる)
今回formatとしてはデフォルトではなくFLOATを使うので、FLOATを指定します。また、今回は良いですが、個別に値を取得する際、補間されては困る場合が多いので、textureFilteringをNEARESTに指定します。データの格納庫として扱う場合は必須です。デフォルトがLINEARなので。
fbo = createFramebuffer({
width:1, height:1, format:FLOAT, textureFiltering:NEAREST
});
なお、内容的にはvec4でしか扱えないので、vec4が一つだけという形式になりますね。そういうわけで1x1となっております。
点描画の準備
使うshaderはこんな感じですね。
#version 300 es
in vec4 aData;
out vec4 vData;
void main(){
vData = aData;
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 1.0;
}
#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
color = vData;
}
適用するテクスチャが1x1なので、中央にぽつんとvec4のデータを置くだけです。これ以上ないくらいのシンプルさですね。なおフロートを配置する点描画の原則として、
gl_PointSize = 1.0
の記述が絶対に必要となります。これを書き忘れたせいで何度も、何度も、何度もバグに悩まされてきたのでみなさんは絶対やらないでください。
次に、データをバッファします。内容的には-1,1,2,-2を入れます。
buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,1,2,-2]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
createBufferでgpuにアクセスするための窓口を作ります。これにFloat32Arrayのデータを紐付ける処理です。値は変更しないのでSTATIC_DRAWを指定します。終わったらbindを解除します。
このバッファ変数を使ってあとでshaderを走らせる際の有効化などを実行します。
(※なお、ここでbuf1として扱っているcreateBufferで生成したオブジェクトというやつはconsole.logなどにぶち込むと致命的なエラーを引き起こすので、くれぐれも実行しないようにしてください。)
浮動小数点数をフレームバッファのテクスチャに格納する
いよいよfboのテクスチャに浮動小数点数を格納します。
fbo.begin();
shader(sh);
_gl._setPointUniforms(sh);
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
sh.enableAttrib(sh.attributes.aData, 4);
gl.drawArrays(gl.POINTS, 0, 1);
sh.unbindShader();
fbo.end();
setPointUniforms()では点描画用のuniformのセッティングを実行しているんですが、実は3D描画の場合の行列uniformなどもここで用意しています(分かりにくいですが)。なのでもし行列変数とか使いたい場合これは必須です。今回は要らないかもしれませんが一応用意してあります。
先ほど用意したバッファをバインドして有効化します。vec4なので二番目の引数のサイズは4となっています。一つ目はshader内部でaDataとして扱っているのでattributesのaDataを用意しています。同じ名前でないといけないんですよ。これで準備は終わりなので、ドローコールを実行します。p5のドローコールは諸事情により使えないのでダイレクトコールで実行しています。1はvec4データが1つだけという意味です。これで、書き込まれました。
書き込まれたことを確かめる
書き込んだらその内容を可視化する必要があります。そのためにbaseMaterialShader()でアトリビュートをいじってみましょう。
mainSh = baseMaterialShader().modify({
'vertexDeclarations':`
uniform sampler2D uData;
`,
'vec3 getLocalPosition':`(vec3 p){
vec4 v = texture(uData, vec2(0.5, 0.5));
p.x += v.x; // x座標を-1
p.y += v.z; // y座標を+2
return p;
}
`
});
vertexDeclarationsでuniformを用意しています。sampler2DでuDataです。これが先ほどのfboのcolorに当たります。
ここまでのコードが正しければ、 v の内容は (-1.0, 1.0, 2.0, -2.0) のはずです。ですから、その x 成分と z 成分を取り出してアトリビュートのx座標とy座標をいじることにします。
実行結果は上に述べた通りです。
function draw() {
background(220);
orbitControl();
shader(mainSh);
mainSh.setUniform("uData", fbo.color);
noStroke();
lights();
fill(0,128,255);
box(100);
}
orbitControl()は特に必要ないんですが気分ということで。あと原因は分かりませんが最初の1フレームだけなぜか反映されないですね。気にはなるんですが今回は不問とします。
発展:複数の浮動小数点数ベクトルを格納し利用する場合
この記事では1x1を分かりやすさのために使いましたが、これだと発展させるのが難しいと思うので、とりあえず2x2の例を挙げておきます。参考になれば幸いです。
p5js float VTF test 2x2
// VTFできましたね
// なんだ~
// 2x2でやろう。いろいろ勉強になるはず。
let fbo;
let sh;
let mainSh;
let buf0, buf1;
const vert =
`#version 300 es
in float aIndex;
in vec4 aData;
out vec4 vData;
void main(){
float x = mod(aIndex, 2.0);
float y = floor(aIndex/2.0);
vec2 uv = (vec2(x,y)+0.5)/2.0;
uv = 2.0*uv - 1.0;
vData = aData;
gl_Position = vec4(uv, 0.0, 1.0);
gl_PointSize = 1.0;
}
`;
const frag =
`#version 300 es
precision highp float;
in vec4 vData;
out vec4 color;
void main(){
color = vData;
}
`;
function setup() {
createCanvas(600, 600, WEBGL);
const _gl = this._renderer;
const gl = _gl.GL;
buf0 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0,1,2,3
]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,1,2,-2, -2,2,3,-3, -3,3,4,-4, -4,4,5,-5
]), gl.STATIC_DRAW);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
sh = createShader(vert, frag);
fbo = createFramebuffer({
width:2, height:2, format:FLOAT, textureFiltering:NEAREST
});
fbo.begin();
shader(sh);
_gl._setPointUniforms(sh);
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
sh.enableAttrib(sh.attributes.aIndex, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
sh.enableAttrib(sh.attributes.aData, 4);
gl.drawArrays(gl.POINTS, 0, 4);
sh.unbindShader();
fbo.end();
mainSh = baseMaterialShader().modify({
'vertexDeclarations':`
uniform sampler2D uData;
uniform vec2 uUv;
`,
'vec3 getLocalPosition':`(vec3 p){
vec4 v = texture(uData, uUv);
p.x += v.x; // x座標を-1
p.y += v.z; // y座標を+2
return p;
}
`
});
}
function draw() {
background(255);
orbitControl();
const uvs = [0.25, 0.25, 0.75, 0.25, 0.25, 0.75, 0.75, 0.75];
for(let k=0; k<4; k++){
shader(mainSh);
mainSh.setUniform("uData", fbo.color);
mainSh.setUniform("uUv", uvs.slice(k*2, (k+1)*2));
noStroke();
lights();
fill(k*32,k*64,255);
box(30);
}
}
点描画のshaderを若干いじっています。というかアトリビュートを増やしていますね。分かりやすさのために組み込み変数のindexは使っていません。0,1,2,3が入っています。これを順に
(-0.5, -0.5), (0.5, -0.5), (-0.5, 0.5), (0.5, 0.5)
にしています。そうしたうえで、それぞれの位置にvec4のベクトル:
(-1,1,2,-2), (-2,2,3,-3), (-3,3,4,-4), (-4,4,5,-5)
をセットするコードです。アトリビュートの追加は、同じ処理をそれぞれのバッファに対して実行するだけなのでそこまで難しくありません。格納用のshaderでマジックナンバーを使ってしまっていますが、uniformで簡単に書き直せるので、まあ、いいですね。
メイン描画ではこれらをすべて使います。新たにテクスチャのuvに相当するuniformを用意し、さっきの順番で並べます。そしてVTFでアタッチして順に描画すると、このように順繰りに並びます:
これを応用すれば256x256でも、200x300でも、何でも同じようにできます。
おわりに
ちょっと難しい内容でしたが、色しか格納できなかったテクスチャに浮動小数点数を格納できるのは面白いので、場合によっては有用かもしれません。ここまでお読みいただいてありがとうございました。