はじめに
動的更新です。これはどっちかというとwebGLバッファの話題なんですが、まあアトリビュートに使うarrayBufferの動的更新が頻度としては一番高いと思うので、ここで取り上げます。内容的にはwgldさんのこの辺です。
VBOを逐次更新しながら描画する
同じ頂点でインデックスバッファだけ書き換えるのもあんま用途が無いと思うので、これにあるようにVBOの更新みたいな感じになるかと思います。他にもUBO...そのうち調べないと。
今回でアトリビュート分野は最後かと思います。色々知るのは楽しいものです。バイト単位でデータを処理しているのを知ったのは最近です。どのサイトに行っても、本読んでも、配列として扱う方が分かりやすいからとそうしているせいで、ずっと知りませんでした。知らないことが、まだまだたくさんあります。テクスチャとTFOについてもそのうち、FBOについてもそのうち取り上げられればと思います(余裕があれば...)。もちろん扱うからには前回のように、変に手を抜いたりせず、厳密に、正確な理解ができるように扱いたいと思います(自分比で)。
3D?3Dは数学なので、きちんとやるのは気が引けるんですよね。難しいので。今日書店に立ち寄ったんですが、ああ本当に行列は高校数学から消えちゃったんだなぁ...と参考書を立ち読みしていて思いました。3DCGこれだけ普及してるのに、行列を知らない高校生を量産する状況に違和感を覚えたりしました。どうするんだろ。自分は高校3年の時、線形代数の本を当時の数学の先生に借りたんですが、あまりに抽象的で難しく感じたので一日で返してしまいました。今でこそ「知って」はいますが、行列の知識があっても線形代数は難しいです。無かったらどうなるのかはあまり想像したくないところです。大学は大変だと思います。
ただ灘高みたいに知りたい人は勝手に勉強すると思うので、問題ないと思います(雑)。
p5.jsでやります。便利なので。ただ前回のあとちょっと実験したんですが、やはり共存は難しいように感じてしまいました。いろいろ隠蔽しすぎのライブラリはネイティブと相性が悪いですね...仕方ないです。
雑談はこのくらいにして、そろそろ本題に入ります。
bufferDataは、データをwebGLBufferに供給すると同時にデータの場所を確保する関数です。メモリアロケーションは時間がかかるので、毎フレームやると負荷が凄いです。いわゆるイミディエイトモードですが、そういう描画で負荷を気にせず柔軟性を獲得する描画方法もあります。カメラのヘルパーとかはそっちの方が便利だったりします。しかし大きいデータだと不便です。具体的には球とか、トーラスとか、テラインとかです。
bufferSubDataという関数があります。これはデータ領域の獲得がすでに終わっているwebGLBufferに対し、バイト単位で中身を書き換える処理です。長さはすでに決まっているので、新たに領域を確保することはしません。ゆえに高速です。
bufferSubData
これを使って内容の更新を実行するのがこの記事の趣旨です。VAOに記録されているバッファであっても更新できるので、余計な手間が増えることもありません。
むしろ、この関数を使う上で負荷が問題になるのはCPUサイドです。たとえば100000個分のデータ更新でも一瞬ですが、100000個分のCPUの計算は相当な負荷になります。GPUサイドではできないような計算の場合に有効な手法ですが、使いどころが肝心だと思います。
寄り道:bufferDataの別の使い方について
その前にbufferDataの用途について補足します。上記のbufferSubDataの作例にも出てきますが、
gl.bufferData(gl.ARRAY_BUFFER, 1024, gl.STATIC_DRAW);
こんな感じで、領域のサイズだけ指定するやり方があります。こうすると1024バイト分の領域が自動的に確保されます。そのうえで、bufferSubDataで改めてデータをぶち込んでいます。あるいは、
gl.bufferData(target, srcData, usage, srcOffset, length)
といったようにoffsetと長さを指定する方法もあります。これについては後述します。
先にバイト数だけ指定する方法は、あとからデータを供給する方が、コードがすっきりする場合に使われます。それはここで紹介する動的更新の他に、GPUサイドでドローコールに基づいて実行するやり方もあって、いわゆるトランスフォームフィードバックですが、そういうのもいずれ紹介できればと思います。
これなんかはSoundDataをトラフィーで計算していて興味深いです。
WebGLでSound Shaderの実装
getBufferSubDataでwebGLBufferの中身を取得したりしています。その辺も取り上げられればと思います。
getBufferSubData
トラフィーと動的更新はいずれもwebGLBufferの中身を書き換える処理で、どうしてもまとめて取り上げたくなってしまうんですが、あっちは話題が重いので、また今度にします。
bufferData, bufferSubData, getBufferSubDataの仕様
ギアル、ギギアル、ギギギアルの仕様。
ギギギアルの仕様!?
はい。
これらの関数は、いずれもwebGLBufferに関係しています。まず初めにbufferDataは、作ったwebGLBufferに対してそれが占める領域を確保すると同時に、場合によってはデータの書き込みを実行します。領域だけ確保する使い方もできます。bufferSubDataはすでに領域を確保したうえで、データを書き換える処理です。getBufferSubDataは、受け皿を何かしら用意したうえで、そこにデータをコピーする形で、webGLBufferの値を読みだす処理です。
復習すると、webGLBufferはgl.createBuffer()で作られるオブジェクトです。WBOとでも略せるかと思います。一番最初に出てきたときはインデックスバッファでしたが、これがいわゆるIBOです。また頂点アトリビュートのデータのソースとして使うならVBOですね。そしてユニフォームバッファオブジェクトというのもあるらしいんですが、略してUBOです。全部まとめてWBOです。普及しそうにないので普通にバッファでいいと思います(雑)。
これらの関数は、WBOへの書き込みとWBOからの読出しを実行します。簡単な例を挙げます。なお、全部一緒なのでターゲットは全部ARRAY_BUFFERとします。だから全部VBOですね。
ああいい忘れてた。
たとえばARRAY_BUFFERとしてWBOをバインドすれば、ターゲットがARRAY_BUFFERである限り書き換えの対象はそのWBOです。またELEMENT_ARRAY_BUFFERならそれですし、出てきてないですがUNIFORM_BUFFERならそれ...です。グローバルステート内で参照しています。読み出す場合も同じです。今までと一緒です。一応、そこに気を付けてお読みください。
最初に共通コードを載せます。
// ターゲット固定用
const TG = 34962; // gl.ARRAY_BUFFER;
const SD = 35044; // gl.STATIC_DRAW;
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
act0(gl);
act0_5(gl);
act1(gl);
act2(gl);
act3(gl);
act4(gl);
}
gl.ARRAY_BUFFERとgl.STATIC_DRAWが何度も出てきて面倒なので、変数で置き換えています。実は、この手のGL定数はすべて整数で代用しても全く問題ないです。挙動を知るのが目的なので、面倒はなるべく避けましょう。
bufferDataとbufferSubDataの基本的な使い方
// 作例0: bufferDataとbufferSubData
// もっとも基本的な使い方
function act0(gl){
const in0 = new Float32Array([1066,2201,-3124,7398]);
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
gl.bufferData(TG, in0, SD);
const out0 = new Float32Array(4);
const out1 = new Uint8Array(16);
const out2 = new Uint16Array(8);
gl.getBufferSubData(TG, 0, out0);
gl.getBufferSubData(TG, 0, out1);
gl.getBufferSubData(TG, 0, out2);
console.log(out0); // 1066, 2201, -3124, 7398
console.log(out1); // コンソール見て
console.log(out2); // コンソール見て
gl.bindBuffer(TG, null);
}
まず普通にデータをバッファします。16バイトです。そこで16バイトの型付配列を3種類用意しました。基本的には、同じサイズなら2番目の引数、オフセットを0にして、3番目の引数に受け皿を用意するだけです。これで書き込まれます。もちろんリトルエンディアンに従って変換された状態で、です。たとえばUint8なら各々のFloat32をリトルエンディアンしてできるバイト単位の整数が順繰りに入ります。
いつぞやのバイト変換が一瞬
そういえば以前、
アトリビュート(2)
において、
[-sin(0), cos(0), -sin(TAU/3), cos(TAU/3), -sin(TAU*2/3), cos(TAU*2/3)]
をバイト変換したと思いますが、これを使えば一瞬です。
function act0_5(gl){
const in0 = new Float32Array([
-sin(0),cos(0),-sin(TAU/3),cos(TAU/3),-sin(TAU*2/3),cos(TAU*2/3)
]);
// どっかで見たなこれ
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
gl.bufferData(TG, in0, SD);
const out0 = new Uint8Array(24);
gl.getBufferSubData(TG, 0, out0);
// https://qiita.com/inaba_darkfox/items/afda47a973998e670b01
// そういうわけです。めっちゃ簡単。
console.log(out0[2],out0[3],out0[4]); // 0,128,0
console.log(out0[9],out0[10],out0[11]); // 179,93,191
console.log(out0[16],out0[17],out0[18]); // 215,179,93
gl.bindBuffer(TG, null);
}
確かにそういう整数が得られていますね...DataViewとどっちが早いかは微妙なところです。しかし、この場合確かめるのが目的なので...でもこういうのをメソッド化したら便利そうではありますね。
ところで似たような関数にreadPixelsというのがあります。あれはテクスチャに対して似たようなことを実行するんですが、毎フレームやるとなかなかに重いんですよね。一般にGPUからCPUへの書き出しを実行する関数は重いので、これも毎フレームやるような処理ではないかもです。逆は、メモリのアロケーションをしないならそれなりに高速なようです。
bufferDataで領域だけ確保して、bufferSubDataでデータを入れる
// 作例1:bufferDataは領域確保だけ、そこにSubDataで入れる
function act1(gl){
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
gl.bufferData(TG, 32, SD); // 32バイトの領域
const in0 = new Float32Array([12, 33, 20, 44, 155, 60, 8, -49]);
gl.bufferSubData(TG, 0, in0);
// 最後の2つだけ書き換える
const in1 = new Float32Array([101, -104]);
gl.bufferSubData(TG, 4*6, in1);
// この場合の書き換え領域は8バイトなので、in1が9バイト以上だとエラーになる。
// その場合、書き換えは一切実行されない。全キャンセル。
// const in1 = new Float32Array([101,-104,999,9999,99999]); エラー
const out0 = new Float32Array(8);
gl.getBufferSubData(TG, 0, out0);
console.log(out0); // 12, 33, 20, 44, 155, 60, 101, -104
// 違うオフセットでやってみる。これはソースのオフセット。バイト指定。
const out1 = new Float32Array(4);
gl.getBufferSubData(TG, 4*3, out1); // 3,4,5,6が入る
// この場合の供給量は20バイトなので、out1が21バイト以上だとエラーになる。
// その場合、何も入らない。部分的に入ることはない。全キャンセル。
// const out1 = new Float32Array(6); // エラー
console.log(out1); // 44, 155, 60, 101
gl.bindBuffer(TG, null);
// まとめ
// bufferSubDataはオフセットに基づいて計算されるdstのバイト領域より
// 大きなデータを用意するとエラーになる。何も実行されない。
// getBufferSubDataはオフセットに基づいて計算されるsrcのバイト領域より
// 大きな受け皿を用意するとエラーになる。何も実行されない。
}
bufferDataは領域だけ確保する使い方ができます。このコードではまず32バイト分の領域を確保しています。そこにまず普通に入れたうえで、長さ8バイトの配列を入れています。オフセットはバイト単位で指定します。これは本当にバイト単位なので、別に4の倍数でなくてもいいんですが、確かめるのが目的なので素直に実行しています。4掛ける6ですから、6,7番目が影響を受けます。それで書き変わっています。
なお、入れるデータの上限はオフセットから計算されるターゲット領域の大きさです。それより大きなバイトデータを入れようとすると、止まりはしないんですが、エラーになり、一つも数が入りません。キャンセルです。今の場合は8バイトを超えるとエラーです。
逆に、読み出し時も制限があり、これもソースのオフセットをバイト単位で指定できるんですが、それにより計算される供給量より受け皿のサイズの方が大きいとエラーになり、何も実行されません。今の場合32バイトのうち12以降なので20バイトですね。これよりたくさんは送れないので、受け皿が20バイトを超えるとエラーになります。
まあ、きちんとサイズを合わせろということですね。ざっくり言うと「入れる場合は全部入れられないとエラー、落とす場合は全部埋められないとエラー」です。サイズが合ってるなら、何も問題は起きません。
bufferDataで部分的にデータを送る場合
// 作例2:bufferDataで部分的に放り込む
function act2(gl){
const in0 = new Float32Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
// 型付配列の場合は単純に配列のインデックスで指定できる。
gl.bufferData(TG, in0, SD, 3, 11);
// なおDataViewの場合はバイト指定。そしてこれができるのはDataViewと型付配列だけ。
const out0 = new Float32Array(11);
gl.getBufferSubData(TG, 0, out0);
console.log(out0);
gl.bindBuffer(TG, null);
}
さて、引数が多い場合です。実はこの場合は簡単な仕組みが用意されています。
bufferDataでデータを送る際にソースの一部だけを送ることができます。この場合、もちろん第2引数が整数だとエラーです(当たり前)。で、後ろの2つで決めるんですが、それぞれoffsetとsizeです。offset番からsize個です。これはなんとバイト指定ではなく、型付配列の場合、配列の番号です。つまりバイト数は関係なく、単純に配列の番号です。これまであれだけバイト単位の指定を意識してきたのに......と思ってしまいますが、まあ素直に安心した方がいいと思います。ちなみにDataViewの場合はきちんとバイト指定です。そしてこの引数が使えるのは型付配列とDataViewの場合だけです。
bufferDataはまだいいですが、問題はbufferSubDataとgetBufferSubDataです。
bufferSubDataで部分的にデータを送る場合
// 作例3:bufferSubDataで部分的に放り込む
function act3(gl){
const in0 = new Float32Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
gl.bufferData(TG, in0, SD);
const in1 = new Float32Array([44,55,66,77,100,200,300,400]);
// offsetはバイト指定だが、型付配列ではstartとsizeはインデックスで指定する。
// DataViewの場合はバイト指定。これができるのはDataViewと型付配列だけ。
// これの場合40バイト目から元の配列の1,2,3,4成分を取って並べる。
// なおソースが大きすぎるとやはりエラーになる。
gl.bufferSubData(TG, 4*10, in1, 1, 4);
const out0 = new Float32Array(16);
gl.getBufferSubData(TG, 0, out0);
console.log(out0);
gl.bindBuffer(TG, null);
}
なんとなく想像は付いたと思いますが、この場合オフセットはバイト指定、ソース配列のコピー部分はインデックスで指定します。ややこしい。まあでも配列ならぶっちゃけその方が指定しやすくて便利だと思います。ちなみにコピー部分が大きすぎて全部入れることができなければ、やはりエラーになります。入れるなら、指定した分すべて入らないといけません。これも同様に、DataViewと型付配列のみ可能です。
getBufferSubDataで部分的にデータを取得する場合
// 作例4:getBufferSubDataで部分的に取り出す
function act4(gl){
const in0 = new Float32Array([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]);
const buf0 = gl.createBuffer();
gl.bindBuffer(TG, buf0);
gl.bufferData(TG, in0, SD);
const out0 = new Float32Array(7);
for(let i=0; i<7; i++){ out0[i] = -1; } // -1で初期化
// 型付配列の場合はインデックスで指定する。この場合、1,2,3,4,5が書き換えられる。
// なおこれによって指定される対象となる領域が大きすぎるとやはりエラーになる。
// これが可能なのはもちろんDataViewと型付配列だけ。
gl.getBufferSubData(TG, 4*2, out0, 1, 5);
console.log(out0);
gl.bindBuffer(TG, null);
}
受け皿としてDataViewや型付配列を使う場合、そこにおけるインデックスを指定することで範囲を指定できます。もちろんオフセットはバイト指定です。なので、FLOATでデータを用意して中途半端なところからスタートしてしかるべき型に変換して数を入れたりできます。楽しいですね。もちろん、指定された受け皿をすべて埋められなければエラーになります。検証は以上になります。
usageのことを忘れてたので追記
bufferDataのサイトにも書いてあるんですが...
bufferData
第3引数のusageは、動的更新の場合、DYNAMIC_DRAWを指定した方がいいようです。
gl.bufferData(gl.ARRAY_BUFFER, 256, gl.DYNAMIC_DRAW);
こんな感じ。変えないならSTATIC_DRAWで、今までは変える機会がなかったんですが、頻繁に変える場合はこっちの方がいいようです。ベンチマーク。え?
さぁ...?
そういうわけで、今後、動的更新の場合はDYNAMIC_DRAWを指定します。
一応、作例
今回も作例を用意します。せっかくなのでVAOで枠組みを作って、恒常ループ内でバッファを更新して動かしてみようと思います。今回はそこまで難しいコードを書きません。きちんと書き変わってることが確認できれば、それで良しとします。
って思ったんですが、割と疲れました。もうやだ...もうやだ...
DYNAMIC_UPDATE
// 動的更新
// しんどい...しんどい...
let loopFunction;
function setup() {
createCanvas(400, 400, WEBGL);
const gl = this._renderer.GL;
const vs_bg =
`#version 300 es
layout (location = 0) in vec2 aPosition;
out vec2 vUv;
void main(){
vUv = aPosition*0.5 + 0.5;
vUv.y = 1.0 - vUv.y;
gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;
const fs_bg =
`#version 300 es
precision highp float;
in vec2 vUv;
out vec4 fragColor;
uniform float uTime;
void main(){
fragColor = vec4(vUv, 0.5 + 0.5*cos(uTime), 1.0);
}
`;
// 背景プログラム
const pg_bg = createShaderProgram(gl, {vs:vs_bg, fs:fs_bg});
const vao_bg = gl.createVertexArray();
gl.bindVertexArray(vao_bg);
const bgPos = 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, bgPos, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.BYTE, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindVertexArray(vao_bg);
const vs =
`#version 300 es
layout (location = 0) in vec2 aPosition;
layout (location = 1) in vec2 aOffsetPosition;
layout (location = 2) in float aOffsetAngle;
layout (location = 3) in vec3 aOffsetColor;
uniform float uTime;
out vec3 vColor;
void main(){
float t = aOffsetAngle + uTime * 2.0;
mat2 m = mat2(cos(t), -sin(t), sin(t), cos(t));
vec2 p = aPosition * m;
p = p * 0.1 + aOffsetPosition;
vColor = (0.5+abs(p.x)) * aOffsetColor;
gl_Position = vec4(p, 0.0, 1.0);
}
`;
const fs =
`#version 300 es
precision highp float;
in vec3 vColor;
out vec4 fragColor;
void main(){
fragColor = vec4(vColor, 1.0);
}
`;
// 三角形プログラム
const pg = createShaderProgram(gl, {vs, fs});
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// position.
const data0 = new Float32Array([-sin(0),cos(0),-sin(TAU/3),cos(TAU/3),-sin(TAU*2/3),cos(TAU*2/3)]);
const buf0 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf0);
gl.bufferData(gl.ARRAY_BUFFER, data0, gl.STATIC_DRAW);
gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
// 動的更新なので領域を確保するだけ
const buf1 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
gl.bufferData(gl.ARRAY_BUFFER, 160, gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(1);
gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(1, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const data2 = new Float32Array(20);
for(let i=0; i<20; i++){ data2[i] = random(TAU); }
const buf2 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf2);
gl.bufferData(gl.ARRAY_BUFFER, data2, gl.STATIC_DRAW);
gl.enableVertexAttribArray(2);
gl.vertexAttribPointer(2, 1, gl.FLOAT, false, 0, 0);
gl.vertexAttribDivisor(2, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
const data3 = new Uint8Array(60);
for(let i=0; i<20; i++){
data3[3*i] = 32;
data3[3*i+1] = 128;
data3[3*i+2] = 220;
}
const buf3 = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf3);
gl.bufferData(gl.ARRAY_BUFFER, data3, gl.STATIC_DRAW);
gl.enableVertexAttribArray(3);
gl.vertexAttribPointer(3, 3, gl.UNSIGNED_BYTE, true, 0, 0);
gl.vertexAttribDivisor(3, 1);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
gl.bindVertexArray(null);
// VAOが機能していることを見るため全部閉じてしまおう
for(let i=0; i<4; i++){
// おりゃあああああああ!!!!!!!!!!!!!
// ひっさつ!でぃすえいぶるばーってくすあとりぶあれー!
// くらえ!
gl.disableVertexAttribArray(i);
}
// 更新用の型付配列
const data1 = new Float32Array(40);
// ここで更新する
const updatePosition = (time) => {
gl.bindBuffer(gl.ARRAY_BUFFER, buf1);
for(let i=0; i<20; i++){
data1[2*i] = (i-10+0.5)/10 + 0.2*cos(time*1.5 + noise(0, i)*9999);
data1[2*i+1] = sin(time*2 + noise(i, 0)*9999);
}
gl.bufferSubData(gl.ARRAY_BUFFER, 0, data1);
gl.bindBuffer(gl.ARRAY_BUFFER, null);
}
loopFunction = (time) => {
gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT|gl.DEPTH_BUFFER_BIT);
// 背景プログラム
gl.useProgram(pg_bg);
gl.bindVertexArray(vao_bg);
setUniformValue(gl, pg_bg, "1f", "uTime", time);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
// 三角形プログラム
gl.useProgram(pg);
gl.bindVertexArray(vao);
// 動的更新
updatePosition(time);
setUniformValue(gl, pg, "1f", "uTime", time);
gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 20);
gl.bindVertexArray(null);
gl.flush();
}
}
function draw(){
loopFunction(millis()/1000);
}
// 以下略
内容はリンク先を確認してください。静止画:
aOffsetPositionだけ動的更新で決定するので、領域確保だけをおこなっています(160バイト)。noise関数を使ってbufferSubDataで書き換えています。以上です。
余談:ロケーションについて
スマホの方で雑に確かめようとしたところ、バグが発生してしまいました。原因はロケーションです。
スマホとパソコンでロケーションが違うのを見たのは初めてかもしれないです。やはりちゃんとロケーションを取得してやらないと駄目ですね...というか単純にlayoutの設定を怠ったせいです。ごめんなさい。スマホの方は小さい方から並べているようです。
おわりに
ここまでお読みいただいてありがとうございました。パフォーマンスについてはよく分かんないので技術の解説だけやりました。あまり意味のない行為でしたがそれなりにいろいろ、モヤモヤしてた部分が解消したので、これでよしとします。前よりちょっとだけアトリビュートに詳しくなった気がします。どうせまじめにやる気は無いので、自己満足出来ればそこまででいいです。もう映えるとか、速いとか、遅いとか、どうでもいいです。どうせCGあんま興味無いので。仕様を調べるのは面白いですが、作品を作るようなことをするのはもうちょっときついのでやめたいですね。
テクスチャとかフレームバッファとか色々調べて自分用に、なんかの役に立つかと思ってまとめようと思っていたんですが、気が変わったというか、あとトランスフォームフィードバックでなんか書いたらそれで終わりかもしれないです。それでいいと思います。