はじめに
いつだったか5年くらい前に
独自アトリビュートを使う実験をしていました。
一部抜粋:
_gl.retainedMode.buffers.fill.push(new p5.RenderBuffer(4, "myAttr1", "myAttr1dst", "aMyAttr1", _gl));
// 第3引数はバッファの場所を示すのでかぶらないように指定しましょう
_gl.retainedMode.buffers.fill.push(new p5.RenderBuffer(4, "myAttr2", "myAttr2dst", "aMyAttr2", _gl));
なお、これをsayoさんが流用してこういうの作ったりしてます。
時は流れて2.3.2になったんですが、もはや「_gl」にアクセスすることは禁止されています。実はアトリビュートを自由に追加する関数ができました。vertexPropertyというp5.Geometryのメソッドです。
これを利用するデモを紹介するのが目的です。
コード全文
let myShader;
function setup() {
createCanvas(400, 400, WEBGL);smooth();
camera(0, 400, 0, 0, 0, 0, 0, 0, 1);
perspective(PI/3, 1, 40*sqrt(3), 4000*sqrt(3));
const geom = new p5.Geometry();
const v = (x,y,z) => {
geom.vertices.push(createVector(x,y,z));
}
const f = (a,b,c) => {
geom.faces.push([a,b,c]);
}
for(let k=0;k<96;k++){
const phi = TAU*k/96;
const cx = 60*cos(phi);
const cy = 60*sin(phi);
const n0 = createVector(cos(phi),sin(phi),0);
const n1 = createVector(0,0,1);
for(let m=0; m<96; m++){
const theta = TAU*m/96;
const x = cx + n0.x*20*cos(theta) + n1.x*20*sin(theta);
const y = cy + n0.y*20*cos(theta) + n1.y*20*sin(theta);
const z = n0.z*20*cos(theta) + n1.z*20*sin(theta);
v(x,y,z);
const diffPos = 5+5*sin(phi*8);
const subX = cx + n0.x*(20+diffPos)*cos(theta) + n1.x*(20+diffPos)*sin(theta);
const subY = cy + n0.y*(20+diffPos)*cos(theta) + n1.y*(20+diffPos)*sin(theta);
const subZ = n0.z*(20+diffPos)*cos(theta) + n1.z*(20+diffPos)*sin(theta);
geom.vertexProperty("aSubPosition", [subX, subY, subZ]);
geom.vertexProperty("aNoiseValue", noise(x/60, y/60, z/60));
}
}
// 0,24,25, 0,25,1, ...
for(let k=0; k<96; k++){
for(let m=0; m<96; m++){
const ld = k*96 + m;
const lu = k*96 + (m<95 ? m+1 : 0);
const rd = (k<95 ? (k+1)*96 + m : m);
const ru = (k<95 ? (m<95 ? (k+1)*96+m+1 : (k+1)*96) : (m<95 ? m+1 : 0));
f(ld,rd,ru); f(ld,ru,lu);
}
}
geom.computeNormals();
myShader = baseMaterialShader().modify({
// Manually specifying a uniform and attribute
vertexDeclarations: 'uniform float time; in vec3 aSubPosition; in float aNoiseValue; out float vNoiseValue;',
fragmentDeclarations: 'in float vNoiseValue;',
'Vertex getWorldInputs': `(Vertex inputs) {
inputs.position = (0.5-0.5*sin(time))*aSubPosition + inputs.position*(0.5+0.5*sin(time));
return inputs;
}`,
'void afterVertex': `() { vNoiseValue = aNoiseValue; }`,
'vec4 getFinalColor': `(vec4 color, vec2 texCoord) { color.rgb *= vNoiseValue * 4.0; return color; }`
});
draw=()=>{
orbitControl();
background(255);
shader(myShader);
myShader.setUniform('time', millis()*TAU/1000);
lights();
noStroke();
fill(45, 99, 200);
model(geom);
}}
トーラスのジオメトリーと追加アトリビュート
実はp5.Geometryの基本的な使い方をZennで紹介しようとしてて、そっちで適当に即席でこしらえた割とディテールの細かいトーラスがあるんで、それ使いますね。で、そのときにvertexPropertyでアトリビュートを追加しようってわけです。
const geom = new p5.Geometry();
const v = (x,y,z) => {
geom.vertices.push(createVector(x,y,z));
}
const f = (a,b,c) => {
geom.faces.push([a,b,c]);
}
for(let k=0;k<96;k++){
const phi = TAU*k/96;
const cx = 60*cos(phi);
const cy = 60*sin(phi);
const n0 = createVector(cos(phi),sin(phi),0);
const n1 = createVector(0,0,1);
for(let m=0; m<96; m++){
const theta = TAU*m/96;
const x = cx + n0.x*20*cos(theta) + n1.x*20*sin(theta);
const y = cy + n0.y*20*cos(theta) + n1.y*20*sin(theta);
const z = n0.z*20*cos(theta) + n1.z*20*sin(theta);
v(x,y,z);
const diffPos = 5+5*sin(phi*8);
const subX = cx + n0.x*(20+diffPos)*cos(theta) + n1.x*(20+diffPos)*sin(theta);
const subY = cy + n0.y*(20+diffPos)*cos(theta) + n1.y*(20+diffPos)*sin(theta);
const subZ = n0.z*(20+diffPos)*cos(theta) + n1.z*(20+diffPos)*sin(theta);
geom.vertexProperty("aSubPosition", [subX, subY, subZ]);
geom.vertexProperty("aNoiseValue", noise(x/60, y/60, z/60));
}
}
// 0,24,25, 0,25,1, ...
for(let k=0; k<96; k++){
for(let m=0; m<96; m++){
const ld = k*96 + m;
const lu = k*96 + (m<95 ? m+1 : 0);
const rd = (k<95 ? (k+1)*96 + m : m);
const ru = (k<95 ? (m<95 ? (k+1)*96+m+1 : (k+1)*96) : (m<95 ? m+1 : 0));
f(ld,rd,ru); f(ld,ru,lu);
}
}
geom.computeNormals();
UV無しのくっついてる方のやつです。それでvertexProperty, 2ヶ所ですね。使っています。これは頂点と同じ順で同じ回数呼び出す必要があります。それでバッファができてアトポンとかいろいろやってくれます。なおロケーションは勝手に決まります。
今回はサブポジション、モディファイした後のポジションと、それとノイズ値ですね。循環するように取っています。なお法線はサブポジに対しては調べていません。同じものを使っています。まあ面倒です。
baseMaterialShaderとmodify
これ癖があって使いにくいんですが、現状追加アトリビュートを改変シェーダで使う方法がこれしかない(調べたけどよくわかんなかった)ので、これに頼ることとします。こんな感じで改変しています。雰囲気を感じてください。
myShader = baseMaterialShader().modify({
// Manually specifying a uniform and attribute
vertexDeclarations: 'uniform float time; in vec3 aSubPosition; in float aNoiseValue; out float vNoiseValue;',
fragmentDeclarations: 'in float vNoiseValue;',
'Vertex getWorldInputs': `(Vertex inputs) {
inputs.position = (0.5-0.5*sin(time))*aSubPosition + inputs.position*(0.5+0.5*sin(time));
return inputs;
}`,
'void afterVertex': `() { vNoiseValue = aNoiseValue; }`,
'vec4 getFinalColor': `(vec4 color, vec2 texCoord) { color.rgb *= vNoiseValue * 4.0; return color; }`
});
まず最初からつまずきます。
どこにもvertexDeclarationsやfragmentDeclarationsが出てきません。しかし通常のdeclarationsではどうやらバーテックスかフラグメントかの区別をしてくれないようで、それだとバリイングが渡せないんですね。それでそのように書いています。不親切だ...なお、以前にbaseMaterialShaderを触った時もここでハマったんですが、その経験が生きました。
それで、Vertex getWorldInputsでpositionの値をいじれるようなので、時間でsubPositionと補間してます。あとはafterVertexでバリイングを渡し、getFinalColorでノイズ値に応じて色を暗くして焦げた感じを出しています。終わりです。
おわりに
追加アトリビュートはそもそもアトリビュートの仕組みが分かってないと使いようが無いんですが、知っていても使うのはなかなか難しい気がします。sayoさんの例やこれのように基本はモーフィングでしょうか。また色の他になんか頂点情報を使いたい場合に重宝するかもしれないですね。
なお追加アトリビュートと言えばインスタンシングですが、現状それについての仕組みは無いようです。
ここまでお読みいただいてありがとうございました。
インスタンシングできるんですか??
できるっぽいね。ただ正規エラーを出されるんで、非推奨なんでしょう。うざ。
まず用意します。7つ描きたいんで、7つ分です。
// インスタンスアトリビュートだぜ
geom.vertexProperty("aFloat", -3);
geom.vertexProperty("aFloat", -2);
geom.vertexProperty("aFloat", -1);
geom.vertexProperty("aFloat", 0);
geom.vertexProperty("aFloat", 1);
geom.vertexProperty("aFloat", 2);
geom.vertexProperty("aFloat", 3);
描画に使うプログラムは、1回でもシェーダーを走らせて描画を実行すれば作られます。作られた後は他のプログラムが起動しない限り据え置きとなるので(今回「線描画」をしていないのでそれも功を奏している)、カレントから取得できます。あとはロケーションとってディバイザーいじって、実はmodel関数の第二引数でインスタンシングできるんです。これこないだチャッピーに教えてもらいました。ありがとうチャッピー。
shader(myShader);
model(geom);
clear();
const pg = gl.getParameter(gl.CURRENT_PROGRAM); // programゲットだぜ
const loc = gl.getAttribLocation(pg, "aFloat"); // ロケーションゲットだぜ
gl.vertexAttribDivisor(loc, 1); // divisorをいじるぜ
draw=()=>{
orbitControl();
background(255);
shader(myShader);
myShader.setUniform('time', millis()*TAU/1000);
lights();
noStroke();
fill(200, 99, 45);
model(geom, 7);
}
できました。
なんかおいしそうっすね。でもこれ駄目みたいで、エラー食らうんだよ。
🌸 p5.jsが言っています: One of the geometries has a custom vertex property 'aFloat' with fewer values than vertices. This is probably caused by directly using the Geometry.vertexProperty() method
ちなみに正規エラーなので、アンフレンドリーです。フレンドリガードが効きません。ガード無効化攻撃です。防ぎようがないです...
要するに頂点数より少ないとエラー食らうんだよ。正規のアトリビュートとしての使い方をしてないと駄目出しされるわけです。インスタンスアトリビュートなら頂点数より少ないのは当たり前なんですが、現状その使い方は考慮されていないようです。でも描画自体は成功するんで、アトポン自体は実行されているのでしょう。ケチだなぁ。
マナーの悪い素行不良の悪ガキなので、こんなことばっかりやってます。ごめんなさい。
追記:modifyのところに書いてあった
ここですね
You can also add a declarations key, where the value is a GLSL string declaring custom uniform variables, globals, and functions shared between hooks. To add declarations just in a vertex or fragment shader, add vertexDeclarations and fragmentDeclarations keys.
なるほど。しかしサンプルで書いてほしいものです。こんなところにちょこっと書かれてもわかんないです。

