はじめに
番外編です。何をやってもいいということなので、トランスフォームフィードバックをやります。何をやっても許される。漫画でも、ライトノベルでも、番外編はそういう位置づけです。そうですよね。なのでトランスフォームフィードバックをやります。大したことはやらないです。600x600の領域で、たった4096個のパーティクルを反射させながら動かすだけの、小さいコードを書くだけです。VAOの説明はもう終わってるので、難しくないです。
TFFとは
トランスフォームフィードバック(TFF)とは、webGLバッファにドローコールを用いてGPUサイドから書き込みを実行できるようにする仕組みのことです。ドローコールは基本POINTSが使われます、というか基本的にこれ以外使いません。これを実行する場合、基本ラスタライズはしません。バーテックスシェーダで頂点ごとに計算して終わりです。その内容をアウトプットサイドに置かれたwebGLバッファへ焼きます。どういう風に焼かれるかというと、単純に小さい方から詰めていくんですが、bindBufferRangeという関数があって、オフセットを指定できます。なので途中からつめたりできるんですが、今回は使わないのでどうでもいいです。
グローバルステート
を、ちょっとだけ拡張します。初歩なのでちょっとだけです。
globalState = {
currentProgram: null,
bindingBuffer: {
arrayBuffer: null,
elementArrayBuffer: null,
uniformBuffer: null, ...
},
bindingVertexArrayObject: null,
vertexAttributeArray: [{},{}, ..., {}], /* 略 */
bufferBase:[
{transformFeedback:{buffer:null, offset:0, size:0},uniform:{/* 略 */}},
{}, {}, ...
],
...
}
bufferBaseというグローバルステートがあります。これはwebgl2で新しく用意された、特別な目的のためのwebGLバッファ用のスロットです。今回はTFFの書き戻し先のバッファをアタッチするための受け皿としてしか使わないので、それ以外の解説はしません。なぜならUBO用のバッファの格納庫としての取り扱いにまだ全く詳しくないため、説明できないからです。
シェーダーを作る際にoutVaryingというのを指定すると、それらを書き戻し先として設定することができます。その、たとえばvec4なら4バイトが4つ分とかなるわけですが、それを格納するバッファをこのbufferBaseにアタッチします。offsetとsizeは要するにどこからどこまでくらいというのを、関数を使って、前回の動的更新みたいに設定できるんですが、今回使わないので両方0でいいです。具体的には、
const program = gl.createProgram();
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
// レイアウト指定はアタッチしてからリンクするまでにやらないと機能しない。
// なおこの機能はwebgl1でも使うことができる。webgl2で実装されたというのは誤解。
setAttributeLayout(gl, program, layout);
setOutVaryings(gl, program, outVaryings, separate);
gl.linkProgram(program);
こんな感じでsetOutVaryings()するわけです。
// TFF用の設定箇所
function setOutVaryings(gl, pg, outVaryings = [], separate = true){
if(outVaryings.length === 0) return;
gl.transformFeedbackVaryings(pg, outVaryings, (separate ? gl.SEPARATE_ATTRIBS : gl.INTERLEAVED_ATTRIBS));
}
INTERLEAVED_ATTRIBSを指定すると、一つのバッファに複数のアウトプットバリイングが順繰りに詰め込まれて、まさにインターリーブ形式で入っていくんですが、今回使わないんで無視してください。セパレートです。
バッファへ書き戻すために、bufferBaseにバッファを紐付ける関数があります。bindBufferBaseといいます。
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
ってやるとbufが0番にセットされて、この0番とかっていうのはvarying配列の番号と一致するんですが、セパレートっていうのはそんな感じで別々のバッファにそれぞれぎゅぎゅっと詰め込まれるんですね。あと範囲指定の関数は、まんまですが、bindBufferRangeといいます。なお第一引数はターゲットです。グローバルステートにも書きましたが、実は各々の部屋は二段ベッドで、二段目がuniformBufferのスペースになっています。今回同居人の挙動はどうでもいいので無視してください。ただ同じ部屋なので、二段目が埋まっていても同じ番号が使えるということは知っておくといいです。テクスチャなんか二段ベッドが二つもあります。大所帯です。ここでは説明しません。
TFO(トランスフォームフィードバックオブジェクト)
次のコードはTFFを理解するうえで非常に参考になります。
transform_feedback_interleaved
TFO(トランスフォームフィードバックオブジェクト)というのがあります。
const tfo = gl.createTransformFeedback();
これはアウトプット版のVAOです。VAOの挙動については死ぬほど詳しく説明したと思いますが、あれの挙動が分かってる人にはすごくわかりやすいです。要するにこれはグローバルステート内の、一部のプロパティの取得と更新を引き受けるんです。一時的に。バインドされてる間だけ。どこかというと、bufferBaseのtransformFeedback枠「すべて」。なので関連する関数は、
- bindBufferBase(但しターゲットはgl.TRANSFORM_FEEDBACK_BUFFER限定)
- bindBufferRange(但しターゲットはgl.TRANSFORM_FEEDBACK_BUFFER限定)
となります。VAOと違ってたったこれだけです。VAOのアウトプット版というのはそういう意味です。
なんとなく察したと思いますが、これはただの記録装置なので、VAOもそうですが、必須ではないです。VAOがアトリビュートを用いた描画に必須でないのと同じく、TFOはトランスフォームフィードバックに必須ではないです。しかし、bufferBaseにおくバッファには、トランスフォームフィードバックの実行時にvertexAttributeArrayの方の枠に同時に置けないという制限があって(詳しい説明は省略します)、そのせいで重宝します。状態を管理するのに使えるわけですね。
番外編なので雑に端折っていきます。じゃあコードを載せます。
コード全文
/*
TFOとVAOを使って交代であれするの作ってください。
できました。ちょろいでした。
*/
const vaoPair = [];
const tfoPair = [];
const NUM = 4096;
let loopFunction = () => {};
function setup() {
createCanvas(600, 600, WEBGL);
pixelDensity(1);
const gl = this._renderer.GL;
// まずTFO用のプログラムを用意する
// 内容的には片方の位置と速度に対して以下略
const vsTFF =
`#version 300 es
layout (location = 0) in vec4 aData;
out vec4 vData;
void main(){
vec2 p = aData.xy;
vec2 v = aData.zw;
if(p.x+v.x>1.0 || p.x+v.x<-1.0){ v.x *= -1.0; }
if(p.y+v.y>1.0 || p.y+v.y<-1.0){ v.y *= -1.0; }
p += v;
vData = vec4(p, v);
}
`;
const fsTFF =
`#version 300 es
void main(){}
`;
const pgTFF = createShaderProgram(gl, {
vs:vsTFF, fs:fsTFF, outVaryings:["vData"]
});
const buf0 = gl.createBuffer();
const data = new Float32Array(4*NUM);
for(let i=0; i<NUM; i++){
data[4*i] = random(-0.99, 0.99);
data[4*i+1] = random(-0.99, 0.99);
const velocity = random(0.0040, 0.0055);
const angle = random(TAU);
data[4*i+2] = velocity*cos(angle);
data[4*i+3] = velocity*sin(angle);
}
// ここではデータを送るだけ
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 同じサイズの領域を作っておく
const buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, NUM*16, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const vsDisplay =
`#version 300 es
layout (location = 0) in vec4 aData;
void main(){
gl_Position = vec4(aData.xy, 0.0, 1.0);
gl_PointSize = 2.0;
}
`;
const fsDisplay =
`#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
`;
const pgDisplay = createShaderProgram(gl, {
vs:vsDisplay, fs:fsDisplay
});
const bufs = [buf0, buf1];
for(let i=0; i<2; i++){
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ARRAY_BUFFER, bufs[i]);
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindVertexArray(null);
vaoPair.push(vao);
}
for(let i=0; i<2; i++){
const tfo = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfo);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufs[i]);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
tfoPair.push(tfo);
}
loopFunction = () => {
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.bindVertexArray(vaoPair[0]);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfoPair[1]);
gl.useProgram(pgTFF);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, NUM);
gl.endTransformFeedback();
gl.disable(gl.RASTERIZER_DISCARD);
gl.bindVertexArray(null);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
gl.bindVertexArray(vaoPair[1]);
gl.useProgram(pgDisplay);
gl.drawArrays(gl.POINTS, 0, NUM);
gl.bindVertexArray(null);
gl.flush();
vaoPair.reverse();
tfoPair.reverse();
}
}
function draw(){
loopFunction();
}
function createShaderProgram(gl, params = {}){
const {vs, fs, layout = {}, outVaryings = [], separate = true} = params;
const vsShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vsShader, vs);
gl.compileShader(vsShader);
if(!gl.getShaderParameter(vsShader, gl.COMPILE_STATUS)){
console.log("vertex shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(vsShader));
return null;
}
const fsShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fsShader, fs);
gl.compileShader(fsShader);
if(!gl.getShaderParameter(fsShader, gl.COMPILE_STATUS)){
console.log("fragment shaderの作成に失敗しました");
console.error(gl.getShaderInfoLog(fsShader));
return null;
}
const program = gl.createProgram();
gl.attachShader(program, vsShader);
gl.attachShader(program, fsShader);
// レイアウト指定はアタッチしてからリンクするまでにやらないと機能しない。
// なおこの機能はwebgl1でも使うことができる。webgl2で実装されたというのは誤解。
setAttributeLayout(gl, program, layout);
setOutVaryings(gl, program, outVaryings, separate);
gl.linkProgram(program);
if(!gl.getProgramParameter(program, gl.LINK_STATUS)){
console.log("programのlinkに失敗しました");
console.error(gl.getProgramInfoLog(program));
return null;
}
// uniform情報を作成時に登録してしまおう
program.uniforms = getActiveUniforms(gl, program);
// attribute情報も登録してしまおう。
program.attributes = getActiveAttributes(gl, program);
return program;
}
// レイアウトの指定。各attributeを配列のどれで使うか決める。
// 指定しない場合はデフォルト値が使われる。基本的には通しで0,1,2,...と付く。
function setAttributeLayout(gl, pg, layout = {}){
const names = Object.keys(layout);
if(names.length === 0) return;
for(const name of names){
const index = layout[name];
gl.bindAttribLocation(pg, index, name);
}
}
// TFF用の設定箇所
function setOutVaryings(gl, pg, outVaryings = [], separate = true){
if(outVaryings.length === 0) return;
gl.transformFeedbackVaryings(pg, outVaryings, (separate ? gl.SEPARATE_ATTRIBS : gl.INTERLEAVED_ATTRIBS));
}
function getActiveUniforms(gl, pg){
const uniforms = {};
// active uniformの個数を取得。
const numActiveUniforms = gl.getProgramParameter(pg, gl.ACTIVE_UNIFORMS);
console.log(`active uniformの個数は${numActiveUniforms}個です`);
for(let i=0; i<numActiveUniforms; i++){
const uniform = gl.getActiveUniform(pg, i);
const location = gl.getUniformLocation(pg, uniform.name);
uniform.location = location;
uniforms[uniform.name] = uniform;
}
return uniforms;
}
function getActiveAttributes(gl, pg){
const attributes = {};
// active attributeの個数を取得。
const numActiveAttributes = gl.getProgramParameter(pg, gl.ACTIVE_ATTRIBUTES);
console.log(`active attributeの個数は${numActiveAttributes}個です`);
for(let i=0; i<numActiveAttributes; i++){
// 取得は難しくない。uniformと似てる。
const attribute = gl.getActiveAttrib(pg, i);
const location = gl.getAttribLocation(pg, attribute.name);
console.log(`${attribute.name}のlocationは${location}です`);
attribute.location = location;
attributes[attribute.name] = attribute;
}
return attributes;
}
TFFのシェーダー
実際にとらん...長いので以下ではTFFでいきます。TFFを実行しているシェーダーはこんなのです。
#version 300 es
layout (location = 0) in vec4 aData;
out vec4 vData;
void main(){
vec2 p = aData.xy;
vec2 v = aData.zw;
if(p.x+v.x>1.0 || p.x+v.x<-1.0){ v.x *= -1.0; }
if(p.y+v.y>1.0 || p.y+v.y<-1.0){ v.y *= -1.0; }
p += v;
vData = vec4(p, v);
}
#version 300 es
void main(){}
バーテックスシェーダでは、インプットのバッファのvec4に対して位置と速度をそれぞれ取り出し、単純な反射処理を施して返しています。インプットとアウトプットは同じサイズのwebGLバッファを使っています。フラグメントシェーダは、なんじゃこりゃと思うかもしれませんが、実はラスタライズをしない場合、これでコンパイルは通ります。ほんとです。
次に描画用です。
#version 300 es
layout (location = 0) in vec4 aData;
void main(){
gl_Position = vec4(aData.xy, 0.0, 1.0);
gl_PointSize = 2.0;
}
#version 300 es
precision highp float;
out vec4 fragColor;
void main(){
fragColor = vec4(1.0);
}
まだやっていませんが点描画です。gl_PointSizeは点の大きさのようなものです。これはdevicePixelRatioに影響を受けるため、今回はpixelDensity(1);を実行しています。
createCanvas(600, 600, WEBGL);
pixelDensity(1);
これでスマホでも同じ見た目になります。もちろんポリゴンとインスタンシングでもいいんですが、めんどくさいのでやりません。
なお、TFFには同時に描画をする書き方も存在するには存在します。これも点描画なのでそれでもいい可能性が、あるにはあるんですが、結局ポリゴンとインスタンシングであればそれは採用できないですし、そういうわけでこのコードでもそこ(更新と描画)は分けて取り扱っています。
バッファの用意
バッファの用意をします。今回、一つの点ごとにvec4ですから、NUM個だとして、NUM*4サイズのFloat32Arrayを用意しています。
const buf0 = gl.createBuffer();
const data = new Float32Array(4*NUM);
for(let i=0; i<NUM; i++){
data[4*i] = random(-0.99, 0.99);
data[4*i+1] = random(-0.99, 0.99);
const velocity = random(0.0040, 0.0055);
const angle = random(TAU);
data[4*i+2] = velocity*cos(angle);
data[4*i+3] = velocity*sin(angle);
}
// ここではデータを送るだけ
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 同じサイズの領域を作っておく
const buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, NUM*16, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
位置と速度を適当にばらけさせて送り込んでいます。これがbuf0に当たります。buf1ですが、交代であっちからこっち、こっちからあっちとやるわけです。しかしその処理はGPU上で行われます。何が言いたいかというと、CPUサイドに領域を作る必要がないです。なのでNUM*16バイトの領域をGPUサイドに作っています。前回やりましたね。それがbuf1です。
VAOとTFOの下準備
今回の流れですが、まずbuf0から読みだしてbuf1に書き込み、buf1で描画。次に、0と1のスワップをして同じ処理、以下同様、です。それをVAOとTFOのとっかえひっかえで実行します。
const bufs = [buf0, buf1];
for(let i=0; i<2; i++){
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
gl.enableVertexAttribArray(0);
gl.bindBuffer(gl.ARRAY_BUFFER, bufs[i]);
gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindVertexArray(null);
vaoPair.push(vao);
}
for(let i=0; i<2; i++){
const tfo = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfo);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, bufs[i]);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
tfoPair.push(tfo);
}
VAAも、bufferBaseも、0番を延々と使いまわします。それを記録させています。もう必要なことは説明し終えているので、詳細は省きます。
描画
TFFの描画がどのように行われるのか説明します。
gl.bindVertexArray(vaoPair[0]);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, tfoPair[1]);
gl.useProgram(pgTFF);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, NUM);
gl.endTransformFeedback();
gl.disable(gl.RASTERIZER_DISCARD);
gl.bindVertexArray(null);
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
gl.bindVertexArray(vaoPair[1]);
gl.useProgram(pgDisplay);
gl.drawArrays(gl.POINTS, 0, NUM);
gl.bindVertexArray(null);
gl.flush();
vaoPair.reverse();
tfoPair.reverse();
ドローコールの前後にbeginとendのTransformFeedbackを実行します。beginサイドにはプリミティブ名を宣言し、ドローコールで同じものを使います...が、普通どっちもgl.POINTSなので適当にgl.POINTSを置いとけばいいです。ラスタライズをしてはいけないのでそれもやっています。ここにあるようにドローコールを挟めばOKです。それをさらにバッファレイアウト(VAO,TFO)でサンドイッチしています。以上です。
次に描画ですが、これもVAOのレイアウトで挟んでるだけです。
最後に、reverseでそれぞれの配列をswap. お疲れ様でした。
おわりに
TFF面白いですね。webGLバッファに書き込む仕組みはcomputeShaderそのもの、っていってもcomputeShaderを見たことが無いので詳しいことは分かんないです。
p5もcomputeShaderについて議論しているようです。
Add compute shaders #7345
わくわくしますね。
ここまでお読みいただいてありがとうございました。
余談
トランスフォームフィードバックのサンプルとして広く流布しているのはこれだと思います。
Transform Feedback で GPGPU
見ればわかりますが、TFOを使っていません。なので毎回bindBufferBaseで割り当てしたり、解除したりしています。また、こっちと違ってバッファのスワップをしています。はっきり言います。バッファのスワップは混乱しやすいです。3つあったら3つともスワップしないといけないわけです。非常に混乱します。あらかじめ割り当て方をいくつか用意しておきさえすれば、割り当て方を交換するだけで済むので簡明です...とはいえTFOの使い方を理解してないとこれはできないので、仕方ないと言えばそうです。
他にもいろんなサンプルを散見しましたが、必要もないのにTFOを作ってほったらかしにしてたり、割と...しかしこのサンプルができた経緯を考えると、動けばいいで作ればそうなるのは仕方ないですね...自分が1年半前に作ったTFFの枠組みはこれを参考にしているんですが、TFOが意味不明だったのでコンパイル時に生成して、outVaryingの処理が終わった後で解除するとかいう理解不能な使い方をしていました。わかると思いますが、outVaryingの処理はTFOとは完全に無関係です。無駄な処理です。知る、というのはとても大事なことですね。
TFOのパートで紹介したサンプルに出会ったのは「transformFeedback instancing」で検索をかけていた時です。TFFとインスタンシングを絡めて使うと変なバグが起きるのを解消できなかったので調べていたんです。そしたら出会いました。調べたら2017年のサンプルなんですね。webgl2リリースと同時に作られることが決まって、割と早くできたのでしょう。wgldがこれに出会っていたらTFOも適切な使い方をされていたかもしれないですね。
これからも気になったことはどんどん調べて、知識をアップデートしていきたいと思いました。
補足:行列出力
一応、アウトプットには行列も指定できることを付記しておきます。
TFF MATRIX OUTPUT
function setup() {
createCanvas(600, 600, WEBGL);
const gl = this._renderer.GL;
// 行列もできるようです
// インターリーブだと普通に順繰りに入りますね
// まあ用途考えればこれでいいでしょうね
const vsTFF =
`#version 300 es
out mat2 vData;
out vec4 vPosition;
void main(){
float fi = float(gl_VertexID);
vData[0] = vec2(fi, 11.0);
vData[1] = vec2(13.0, 74.0);
vPosition = vec4(-4.0, fi, 8.0, 12.375);
}
`;
const fsTFF =
`#version 300 es
void main(){}
`;
// インターリーブにするにはseparateをfalseにする
const pgTFF = createShaderProgram(gl, {
vs:vsTFF, fs:fsTFF, outVaryings:["vData", "vPosition"], separate:false
});
// 96バイトの領域を作っておく
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.bufferData(gl.ARRAY_BUFFER, 96, gl.DYNAMIC_COPY);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 今回はTFO使わないんでこれで。
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, buf);
gl.useProgram(pgTFF);
gl.beginTransformFeedback(gl.POINTS);
gl.enable(gl.RASTERIZER_DISCARD);
gl.drawArrays(gl.POINTS, 0, 3);
gl.disable(gl.RASTERIZER_DISCARD);
gl.endTransformFeedback();
// まあ不便ですよね。数が増えてくるとどんどん煩雑になっていくのでお勧めしないです。
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, 0, null);
const fa = new Float32Array(24);
// getBufferSubDataを使えばお手軽に結果を確認できます。
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
gl.getBufferSubData(gl.ARRAY_BUFFER, 0, fa);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
console.log(fa);
}
/* 以下略 */
TFOを使わない場合、バッファが増えるごとにあのサンドイッチが分厚くなっていくんですよね...まあそれはいいとして、こんな感じです。今回はインターリーブにしました。separateをfalseにするとインターリーブになります。行列データとベクトルデータが順繰りに詰めて入ります。こんな感じです。応用が思いつかないですが、できることを知っておくのは大事なように感じました。
一応、Vanillaというかnon-p5.jsのコードを置いておきます。
CodePen:TFF 4096 particle
パフォーマンスはあんま変わんないです。
p5は高度な機能なんか要らないんですよね。これ以上webgl進化させても仕方ない気がします。TLでp5のコード書いてる人たちを見てもそういうのを使ってないわけです。需要と供給が全くマッチしてないように感じます。p5は高度なwebglなんかより今のお手軽楽ちんでいいんだと思います。今これを書いてるのもp5のためではなく自分がwebglのモヤモヤしてる部分を解消するためです。p5のアイデンティティを否定するようなことをしたいわけではないです。
旅の恥はかき捨て
まあこんな感じですね。
// isTFで分岐処理。
const isTF = (outVaryings.length > 0);
// isTFならTFの準備をする。
let transformFeedback;
if (isTF) {
transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
}
const vShader = _getShader(name, gl, sourceV, "vs");
const fShader = _getShader(name, gl, sourceF, "fs");
// プログラムの作成
let _program = gl.createProgram();
// シェーダーにアタッチ → リンク
gl.attachShader(_program, vShader);
gl.attachShader(_program, fShader);
// TFの場合はoutVaryingsによりちょっと複雑な処理をする。
// インターリーブは未習得なのでパス。
if (isTF) {
gl.transformFeedbackVaryings(_program, outVaryings, gl.SEPARATE_ATTRIBS);
}
gl.linkProgram(_program);
// 結果のチェック
if(!gl.getProgramParameter(_program, gl.LINK_STATUS)){
myAlert('Could not initialize shaders. ' + "name: " + name + ", program link failure.");
return null;
}
if (isTF) {
// 必要かどうかは不明。
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
}
return _program;
バリイング配列の長さが正かどうかで分けて、正なら「ああTFFですね」ってことで、transformFeedbackObjectを生成してるんですよね。で、最後に破棄してますよね。
無駄。
まあ知らないでやればそうなります。で、この後せっかく作ったTFOは全く見向きもされないわけです。あとインターリーブを当たり前のように敬遠してますが、単純に何だろう、腫れ物を扱うようなイメージでTFFを触ってたんですよね。なんかいろいろ意味不明、不安というか、このTFO、が何をしているのかも分かんないし、いろいろ不安だったんだと思います。サイト、上に挙げたのですけど、サイトでもなんか「めっちゃ難しいめっちゃすごい」みたいに取り上げていてそれを真に受けたりね。そういうの結構影響するんですよね。実際は、
webGLBufferの中身をドローコールで書き換えるだけ、単純にそれだけなのに。
知らないってのはそういうことです。で、今知ってる状態で、どこまで理解してるのかがはっきりしてる状態で、もちろんまだ先があるかもしれないんですけどそれは重要じゃないんですよね。大事なことは何か?
ここまでは理解したとはっきり言えること。
数学ガールに「自分の理解の最前線」って言葉があるんですけどまさにそれで、今webGLこうやって一から組み立ててるのもそれをはっきりさせるためなんですよね。台車に荷物を積むじゃないですか。綺麗に積まないと後から来た荷物が置けなくて「詰む」んですよね。それが嫌でこうしてあれこれしてるわけです。
サイトの方も大概ですけどね。
// transform feedback object
var transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
// out variable names
var outVaryings = ['gl_Position', 'vColor'];
// transform out shader
var vs = create_shader('vs_transformOut');
var fs = create_shader('fs_transformOut');
var prg = create_program_tf_separate(vs, fs, outVaryings);
見れば明らかだと思いますがこれを真似したわけです。で、こっちはというと、ここでbindして、
ほったらかし。
おわりです。記録して、記録し続けて、それで終わりです。この子(TFO)は記録し続けるだけの人生です。まあそんな感じなので、知るって大事だなって思いました。
で、次の記事です。
【WebGL2】GPU Instancing x Transform Feedback で大量のインスタンスの計算と描画をGPUで行う
カヤックさんの記事は面白い内容のものが多いので時々読ませていただいてるんですが、これは初めて見たかもしれないです...次の箇所:
const transformFeedback = gl.createTransformFeedback();
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, transformFeedback);
for (let i = 0; i < buffers.length; i++) {
const buffer = buffers[i];
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bindBufferBase(gl.TRANSFORM_FEEDBACK_BUFFER, i, buffer);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
gl.bindTransformFeedback(gl.TRANSFORM_FEEDBACK, null);
そうですね、ARRAY_BUFFERに紐付ける必要はないんですが...でも使い方は合ってますね。で、恒常ループでもこの記事のようにちゃんと呼び出す形で使っていますね。そして当然ですが、bindBufferBaseは恒常ループでは一切使っていません。正しい使い方です。っていうか参考にしたサイトの中にwgldがあるんですが、これ参考にしてこの使い方はおかしいですね...多分、自分なりにTFOが何なのか何らかの理由で見出したんだと思います。こういう姿勢を見習いたいと思いました。
ああでもちょっとこのやり方そっくりそのままではやりたくないかもですね...バッファ自体のスワップは頭が混乱するので。こういうのは流儀だと思うんですが、真似したくないかもですね。
動かすのはめっちゃ真似したい...勉強しないと。
以上です。
参考:p5のライティングのフラグメントシェーダ(一般的なケース)
番外編なので何でもありということで、今現在p5が通常のライティングをする場合にどんなシェーダが使われているのかお見せしたいと思います。
lights();
fill(255);
noStroke();
sphere(50);
だとして、これ:
#version 300 es
#define WEBGL2
#define FRAGMENT_SHADER
precision highp float;
#ifdef WEBGL2
#define IN in
#define OUT out
#ifdef FRAGMENT_SHADER
out vec4 outColor;
#define OUT_COLOR outColor
#endif
#define TEXTURE texture
#else
#ifdef FRAGMENT_SHADER
#define IN varying
#else
#define IN attribute
#endif
#define OUT varying
#define TEXTURE texture2D
#ifdef FRAGMENT_SHADER
#define OUT_COLOR gl_FragColor
#endif
#endif
#define PI 3.141592
precision highp float;
precision highp int;
uniform mat4 uViewMatrix;
uniform bool uUseLighting;
uniform int uAmbientLightCount;
uniform vec3 uAmbientColor[5];
uniform mat3 uCameraRotation;
uniform int uDirectionalLightCount;
uniform vec3 uLightingDirection[5];
uniform vec3 uDirectionalDiffuseColors[5];
uniform vec3 uDirectionalSpecularColors[5];
uniform int uPointLightCount;
uniform vec3 uPointLightLocation[5];
uniform vec3 uPointLightDiffuseColors[5];
uniform vec3 uPointLightSpecularColors[5];
uniform int uSpotLightCount;
uniform float uSpotLightAngle[5];
uniform float uSpotLightConc[5];
uniform vec3 uSpotLightDiffuseColors[5];
uniform vec3 uSpotLightSpecularColors[5];
uniform vec3 uSpotLightLocation[5];
uniform vec3 uSpotLightDirection[5];
uniform bool uSpecular;
uniform float uShininess;
uniform float uMetallic;
uniform float uConstantAttenuation;
uniform float uLinearAttenuation;
uniform float uQuadraticAttenuation;
// setting from _setImageLightUniforms()
// boolean to initiate the calculateImageDiffuse and calculateImageSpecular
uniform bool uUseImageLight;
// texture for use in calculateImageDiffuse
uniform sampler2D environmentMapDiffused;
// texture for use in calculateImageSpecular
uniform sampler2D environmentMapSpecular;
const float specularFactor = 2.0;
const float diffuseFactor = 0.73;
struct LightResult {
float specular;
float diffuse;
};
float _phongSpecular(
vec3 lightDirection,
vec3 viewDirection,
vec3 surfaceNormal,
float shininess) {
vec3 R = reflect(lightDirection, surfaceNormal);
return pow(max(0.0, dot(R, viewDirection)), shininess);
}
float _lambertDiffuse(vec3 lightDirection, vec3 surfaceNormal) {
return max(0.0, dot(-lightDirection, surfaceNormal));
}
LightResult _light(vec3 viewDirection, vec3 normal, vec3 lightVector, float shininess, float metallic) {
vec3 lightDir = normalize(lightVector);
//compute our diffuse & specular terms
LightResult lr;
float specularIntensity = mix(1.0, 0.4, metallic);
float diffuseIntensity = mix(1.0, 0.1, metallic);
if (uSpecular)
lr.specular = (_phongSpecular(lightDir, viewDirection, normal, shininess)) * specularIntensity;
lr.diffuse = _lambertDiffuse(lightDir, normal) * diffuseIntensity;
return lr;
}
// converts the range of "value" from [min1 to max1] to [min2 to max2]
float map(float value, float min1, float max1, float min2, float max2) {
return min2 + (value - min1) * (max2 - min2) / (max1 - min1);
}
vec2 mapTextureToNormal( vec3 v ){
// x = r sin(phi) cos(theta)
// y = r cos(phi)
// z = r sin(phi) sin(theta)
float phi = acos( v.y );
// if phi is 0, then there are no x, z components
float theta = 0.0;
// else
theta = acos(v.x / sin(phi));
float sinTheta = v.z / sin(phi);
if (sinTheta < 0.0) {
// Turn it into -theta, but in the 0-2PI range
theta = 2.0 * PI - theta;
}
theta = theta / (2.0 * 3.14159);
phi = phi / 3.14159 ;
vec2 angles = vec2( fract(theta + 0.25), 1.0 - phi );
return angles;
}
vec3 calculateImageDiffuse(vec3 vNormal, vec3 vViewPosition, float metallic){
// make 2 seperate builds
vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0); // hardcoded world camera position
vec3 worldNormal = normalize(vNormal * uCameraRotation);
vec2 newTexCoor = mapTextureToNormal( worldNormal );
vec4 texture = TEXTURE( environmentMapDiffused, newTexCoor );
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return mix(smoothstep(vec3(0.0), vec3(1.0), texture.xyz), vec3(0.0), metallic);
}
vec3 calculateImageSpecular(vec3 vNormal, vec3 vViewPosition, float shininess, float metallic){
vec3 worldCameraPosition = vec3(0.0, 0.0, 0.0);
vec3 worldNormal = normalize(vNormal);
vec3 lightDirection = normalize( vViewPosition - worldCameraPosition );
vec3 R = reflect(lightDirection, worldNormal) * uCameraRotation;
vec2 newTexCoor = mapTextureToNormal( R );
#ifdef WEBGL2
// In p5js the range of shininess is >= 1,
// Therefore roughness range will be ([0,1]*8)*20 or [0, 160]
// The factor of 8 is because currently the getSpecularTexture
// only calculated 8 different levels of roughness
// The factor of 20 is just to spread up this range so that,
// [1, max] of shininess is converted to [0,160] of roughness
float roughness = 20. / shininess;
vec4 outColor = textureLod(environmentMapSpecular, newTexCoor, roughness * 8.);
#else
vec4 outColor = TEXTURE(environmentMapSpecular, newTexCoor);
#endif
// this is to make the darker sections more dark
// png and jpg usually flatten the brightness so it is to reverse that
return mix(
pow(outColor.xyz, vec3(10)),
pow(outColor.xyz, vec3(1.2)),
metallic
);
}
void totalLight(
vec3 modelPosition,
vec3 normal,
float shininess,
float metallic,
out vec3 totalDiffuse,
out vec3 totalSpecular
) {
totalSpecular = vec3(0.0);
if (!uUseLighting) {
totalDiffuse = vec3(1.0);
return;
}
totalDiffuse = vec3(0.0);
vec3 viewDirection = normalize(-modelPosition);
for (int j = 0; j < 5; j++) {
if (j < uDirectionalLightCount) {
vec3 lightVector = (uViewMatrix * vec4(uLightingDirection[j], 0.0)).xyz;
vec3 lightColor = uDirectionalDiffuseColors[j];
vec3 specularColor = uDirectionalSpecularColors[j];
LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor;
totalSpecular += result.specular * lightColor * specularColor;
}
if (j < uPointLightCount) {
vec3 lightPosition = (uViewMatrix * vec4(uPointLightLocation[j], 1.0)).xyz;
vec3 lightVector = modelPosition - lightPosition;
//calculate attenuation
float lightDistance = length(lightVector);
float lightFalloff = 1.0 / (uConstantAttenuation + lightDistance * uLinearAttenuation + (lightDistance * lightDistance) * uQuadraticAttenuation);
vec3 lightColor = lightFalloff * uPointLightDiffuseColors[j];
vec3 specularColor = lightFalloff * uPointLightSpecularColors[j];
LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor;
totalSpecular += result.specular * lightColor * specularColor;
}
if(j < uSpotLightCount) {
vec3 lightPosition = (uViewMatrix * vec4(uSpotLightLocation[j], 1.0)).xyz;
vec3 lightVector = modelPosition - lightPosition;
float lightDistance = length(lightVector);
float lightFalloff = 1.0 / (uConstantAttenuation + lightDistance * uLinearAttenuation + (lightDistance * lightDistance) * uQuadraticAttenuation);
vec3 lightDirection = (uViewMatrix * vec4(uSpotLightDirection[j], 0.0)).xyz;
float spotDot = dot(normalize(lightVector), normalize(lightDirection));
float spotFalloff;
if(spotDot < uSpotLightAngle[j]) {
spotFalloff = 0.0;
}
else {
spotFalloff = pow(spotDot, uSpotLightConc[j]);
}
lightFalloff *= spotFalloff;
vec3 lightColor = uSpotLightDiffuseColors[j];
vec3 specularColor = uSpotLightSpecularColors[j];
LightResult result = _light(viewDirection, normal, lightVector, shininess, metallic);
totalDiffuse += result.diffuse * lightColor * lightFalloff;
totalSpecular += result.specular * lightColor * specularColor * lightFalloff;
}
}
if( uUseImageLight ){
totalDiffuse += calculateImageDiffuse(normal, modelPosition, metallic);
totalSpecular += calculateImageSpecular(normal, modelPosition, shininess, metallic);
}
totalDiffuse *= diffuseFactor;
totalSpecular *= specularFactor;
}
// include lighting.glsl
precision highp int;
uniform bool uHasSetAmbient;
uniform vec4 uSpecularMatColor;
uniform vec4 uAmbientMatColor;
uniform vec4 uEmissiveMatColor;
uniform vec4 uTint;
uniform sampler2D uSampler;
uniform bool isTexture;
IN vec3 vNormal;
IN vec2 vTexCoord;
IN vec3 vViewPosition;
IN vec3 vAmbientColor;
IN vec4 vColor;
struct ColorComponents {
vec3 baseColor;
float opacity;
vec3 ambientColor;
vec3 specularColor;
vec3 diffuse;
vec3 ambient;
vec3 specular;
vec3 emissive;
};
struct Inputs {
vec3 normal;
vec2 texCoord;
vec3 ambientLight;
vec3 ambientMaterial;
vec3 specularMaterial;
vec3 emissiveMaterial;
vec4 color;
float shininess;
float metalness;
};
void main(void) {
HOOK_beforeFragment();
Inputs inputs;
inputs.normal = normalize(vNormal);
inputs.texCoord = vTexCoord;
inputs.ambientLight = vAmbientColor;
inputs.color = isTexture
// Textures come in with premultiplied alpha. To apply tint and still have
// premultiplied alpha output, we need to multiply the RGB channels by the
// tint RGB, and all channels by the tint alpha.
? TEXTURE(uSampler, vTexCoord) * vec4(uTint.rgb/255., 1.) * (uTint.a/255.)
// Colors come in with unmultiplied alpha, so we need to multiply the RGB
// channels by alpha to convert it to premultiplied alpha.
: vec4(vColor.rgb * vColor.a, vColor.a);
inputs.shininess = uShininess;
inputs.metalness = uMetallic;
inputs.ambientMaterial = uHasSetAmbient ? uAmbientMatColor.rgb : inputs.color.rgb;
inputs.specularMaterial = uSpecularMatColor.rgb;
inputs.emissiveMaterial = uEmissiveMatColor.rgb;
inputs = HOOK_getPixelInputs(inputs);
vec3 diffuse;
vec3 specular;
totalLight(vViewPosition, inputs.normal, inputs.shininess, inputs.metalness, diffuse, specular);
// Calculating final color as result of all lights (plus emissive term).
vec2 texCoord = inputs.texCoord;
vec4 baseColor = inputs.color;
ColorComponents c;
c.opacity = baseColor.a;
c.baseColor = baseColor.rgb;
c.ambientColor = inputs.ambientMaterial;
c.specularColor = inputs.specularMaterial;
c.diffuse = diffuse;
c.ambient = inputs.ambientLight;
c.specular = specular;
c.emissive = inputs.emissiveMaterial;
OUT_COLOR = HOOK_getFinalColor(HOOK_combineColors(c));
HOOK_afterFragment();
}
上の方の#defineはほとんど機能してないです。こんなにぶくぶく太ったのは、開発時にいろいろ話を聞かせてもらったんですが、なんだろ。シェーダーが増えると保守がめんどくさいからだそうです。それで、全部乗せ。しかもこれに加えてHOOKの翻訳が入るんですよね。どんどん重くなっていくわけです。フラグメントシェーダはピクセルごとに実行するので、一番描画においてボトルネックになりやすい部分です。ただp5はこれでいいと思います。速さより手軽さ。それがp5なのです。