はじめに
p5.jsの立方体のテクスチャはすべての面が同じUVを持っています。しかしそれでは不都合な場合もあるでしょう。各面に違う色を塗りたい場合です。しかし簡単な方法が無いので、困ってしまうわけです。やり方を2つ紹介します。
コード
方法1. 平面を6枚描く
各面に別々の色を設定するのが面倒なら、各面別々に描けばいいわけですね。それを実行します。
function setup() {
createCanvas(400,400,WEBGL);
smooth();
noStroke();
const zAngles = [0, 0,PI/2,PI,PI*3/2,0];
const xAngles = [0, PI/2,PI/2,PI/2,PI/2,PI];
const palette = ['red','orange','blue','green','purple','yellow'];
draw=()=>{
background(0);
orbitControl(1,1,1,{freeRotation:true});
for(let i=0; i<6; i++){
push();
rotateZ(zAngles[i]);
rotateX(xAngles[i]);
translate(0,0,50);
fill(palette[i]);
plane(100);
pop();
}
}
}
なお今回ライティングはしません。
こういう実験をするときstrokeが邪魔なのでいちいちnoStroke()やるんですが...早くWebGLのnoStroke()がデフォルトにならないかとやきもきします。
それで、回転についてですが、ローカル軸をいじって座標系を動かしています。トランスレーション方向をいちいち定めるのが面倒なので、先に回転させてしまってz軸正方向にとばして描画しています。なんとなくイメージできるかと思います。
この方法の利点は平面描画の仕組みをそのまま使えることですね。テクスチャも別々に設定できます。便利です。1個しか描かないならこれで充分かと思います。
欠点は複数描く場合に負荷が無視できなくなるところです。なお静止画の場合は何の問題もありません。動画の場合、たとえば100個描くなら600回もplaneを呼び出すわけです。そういう問題はありますね。
方法2. texture2DArrayを使う
次の記事で紹介されている方法を使います。
ただ、下の記事で紹介されているmodify関連の記述が古くて役に立たないので...
const sh = baseMaterialShader().modify({
'fragmentDeclarations':`
precision mediump sampler2DArray;
uniform sampler2DArray uFish;
uniform float uFrame;
`,
'vec4 getFinalColor':`(vec4 color){
color = texture(uFish, vec3(vTexCoord, uFrame));
return color;
}`
});
ここですね。getFinalColorは仕様が変更され、テクスチャのuvを取得できるようになりました。こっちの記事ではそうしていますね。
こないだ書いた記事です。
const sh = baseMaterialShader().modify({
'vec4 getFinalColor':`(vec4 color, vec2 texCoord){
vec2 uv = texCoord;
if(gl_FrontFacing){
color = texture(uSampler, vec2(uv.x*0.5, 1.0-uv.y));
}else{
color = texture(uSampler, vec2(uv.x*0.5+0.5, uv.y));
}
return color;
}`
});
なのでこっちを使って適宜修正します。p5は仕様変更が多くて大変ですね...なおこのbaseMaterialShaderはそういう機能なのでそこら辺は仕方ないというのもあります。成長中なので試行錯誤が多いのでしょう。
出来ました。
function setup() {
createCanvas(400,400,WEBGL);
smooth();
noStroke();
const gl = drawingContext;
const cube = buildGeometry(()=>{box(100);});
const uvs = cube.uvs;
for(let i=0; i<24; i++){
const faceIndex = Math.floor(i/4);
cube.vertexProperty('aMyTex', [uvs[2*i], uvs[2*i+1], faceIndex]);
}
// shaderを作る
const sh = baseMaterialShader().modify({
vertexDeclarations:'in vec3 aMyTex; out vec3 vMyTex;',
'void afterVertex': `() { vMyTex = aMyTex; }`,
'fragmentDeclarations':`
precision highp sampler2DArray;
uniform sampler2DArray uTex;
in vec3 vMyTex;
`,
'vec4 getFinalColor':`(vec4 color, vec2 texCoord){
color = texture(uTex, vMyTex);
return color;
}`
});
const tex = createMonoColorTextures(gl, 4, [
'red','orange','blue','green','purple','yellow'
]);
shader(sh); // これでprogramができる
const pg = sh._glProgram;
gl.useProgram(pg); // これでprogramが走る
const loc = gl.getUniformLocation(pg, "uTex");
// 4番に入れる。
gl.uniform1i(loc, 4);
draw=()=>{
shader(sh);
background(0);
orbitControl(1,1,1,{freeRotation:true});
fill(0);
model(cube);
}
}
function createMonoColorTextures(gl, index = 0, palette = []){
const tex = gl.createTexture();
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);
gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, 40, 40, 6, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
const offscreen = createGraphics(40,40);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
for(let i=0; i<6; i++){
offscreen.background(palette[i]);
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, i, 40, 40, 1, gl.RGBA, gl.UNSIGNED_BYTE, offscreen.elt);
}
return tex;
}
方法2の解説
上の方でまずbuildGeometryによりboxを作っています。これにより生成されるジオメトリは大きさが固定されるので、あらかじめ100で作っておく必要があります。同じ大きさになります。一辺が100の立方体です。
それでuvですが、uvの構造は「各面ずつ4つのuvの成分が8つ並ぶのが6つの面ごとにそれぞれ格納」という形式です。こういうのは普通レファレンスとかに載っているといじりやすくて助かるんですが、まあ解析すればわかることなのでどうでもいいですね。それで、それをちょっといじります。vertexPropertyで独自uvを作ります。
for(let i=0; i<24; i++){
const faceIndex = Math.floor(i/4);
cube.vertexProperty('aMyTex', [uvs[2*i], uvs[2*i+1], faceIndex]);
}
具体的にはこのように、uvを8つずつ取り出して、面ごとに3つ目の座標を0,1,2,3,4,5と付与します。これにより、vec3の新しいアトリビュート「aMyTex」が爆誕します。これをシェーダーで使うんですが、どう使うかというと、texture2DArrayの第3引数でこれを使います。つまり面ごとに違うレイヤーのテクスチャを適用するわけですね。
texture2DArrayの生成はここでやっています。
function createMonoColorTextures(gl, index = 0, palette = []){
const tex = gl.createTexture();
gl.activeTexture(gl.TEXTURE0 + index);
gl.bindTexture(gl.TEXTURE_2D_ARRAY, tex);
gl.texImage3D(gl.TEXTURE_2D_ARRAY, 0, gl.RGBA, 40, 40, 6, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
const offscreen = createGraphics(40,40);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D_ARRAY, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
for(let i=0; i<6; i++){
offscreen.background(palette[i]);
gl.texSubImage3D(gl.TEXTURE_2D_ARRAY, 0, 0, 0, i, 40, 40, 1, gl.RGBA, gl.UNSIGNED_BYTE, offscreen.elt);
}
return tex;
}
詳しい解説は省略しますが、Subを使うと面ごとにソースを決められるとかそんな感じですね。createGraphicsで作ったキャンバスからはeltでソースをゲットできるんでそれを当てはめます。面ごとの配色は並びが違っていますがこれはp5サイドが決めています。どういう順番なのか解析してみます。
red: x-
orange: x+
blue: y-
green: y+
purple: z-
yellow: z+
そういうわけでx-,x+,y-,y+,z-,z+の順ですね。割と分かりやすいです。
これをtextureSlotの4番にぶち込むんですが、まずshader(sh)でプログラムが生成されるということに留意します。こうします。
shader(sh); // これでprogramができる
const pg = sh._glProgram;
gl.useProgram(pg); // これでprogramが走る
const loc = gl.getUniformLocation(pg, "uTex");
// 4番に入れる。
gl.uniform1i(loc, 4);
いいかげん「getProgram()」とか欲しい所ですが...それで、uTexに入れます。なおvec3になるかどうかですが、なります。宣言でそういう風に使えば、数が足りていれば自動的にそうなります。
// shaderを作る
const sh = baseMaterialShader().modify({
vertexDeclarations:'in vec3 aMyTex; out vec3 vMyTex;',
'void afterVertex': `() { vMyTex = aMyTex; }`,
'fragmentDeclarations':`
precision highp sampler2DArray;
uniform sampler2DArray uTex;
in vec3 vMyTex;
`,
'vec4 getFinalColor':`(vec4 color, vec2 texCoord){
color = texture(uTex, vMyTex);
return color;
}`
});
2DArrayのテクスチャはプレシジョン(精度)宣言が必須です。それで、vec3のaMyTexをvMyTexで送り、あとは普通にtextureします。texCoordは無視します。2Dでは各面別々にできません。これでおわりです。
この方法の欠点は手続きが面倒なことですね。まあこっちの好きなようにやろうとすれば、そうなるのは仕方ないです。万人に使いやすいシステムなどは無いし、コアな機能を作ってもそれを欲している人が少ないと保守管理のコストとかそういう関係で実現しないこともあるでしょう。
利点は負荷が小さいことですね。ドローコールだけでも単純に1/6なのでエコです。
向きを調べてみる。
ついでに向きを調べてみました。
さっきのグラフィックをちょこっといじって中央に色名を付与しました。キューブマップを見慣れている自分から見ると、なんというか、なんだろ...表と裏で向きが異なっているとモヤモヤしますね...まあいいか。
単色で運用するなら、向きはどうでもいいでしょう。
余談: p5のconeを調べてみよう
ページが余ったのでp5.jsのconeで三角錐を作って遊びましょう。
function setup() {
createCanvas(400,400,WEBGL);
smooth();
const gl = drawingContext;
const myCone = buildGeometry(()=>{cone(100,100,4,1);});
console.log(myCone.vertices);
draw=()=>{
background(0);
orbitControl(1,1,1,{freeRotation:true});
fill(255);
model(myCone);
}
}
横のdetailを4にすると三角錐になります。高さを調節すれば正四面体になります。さて問題です。
Q. このジオメトリの頂点の個数はいくつでしょうか?
見た感じ4つか5つくらいに見えます。実際に確かめるには、verticesを覗けばいいですね。さっそくやってみましょう。
Array(3) [ 0, -50, 0 ]
Array(3) [ 0, -50, 0 ]
Array(3) [ 0, -50, 0 ]
Array(3) [ 0, -50, 0 ]
Array(3) [ 0, -50, 100 ]
Array(3) [ 86.60254037844388, -50, -49.99999999999998 ]
Array(3) [ -86.60254037844385, -50, -50.00000000000004 ]
Array(3) [ -2.4492935982947064e-14, -50, 100 ]
Array(3) [ 0, -50, 100 ]
Array(3) [ 86.60254037844388, -50, -49.99999999999998 ]
Array(3) [ -86.60254037844385, -50, -50.00000000000004 ]
Array(3) [ -2.4492935982947064e-14, -50, 100 ]
Array(3) [ 0, 50, 0 ]
Array(3) [ 0, 50, 0 ]
Array(3) [ 0, 50, 0 ]
Array(3) [ 0, 50, 0 ]
答え:16個。
同じ点の重複が多いので16個です。ちょっとした引っ掛け問題でした。では次の問題。
Q. このジオメトリの面の個数はいくつでしょうか?
これなら簡単ですね。6以外ありえません。orbitControlで舐めるように目を凝らせば6つの面が見えます。これ以外ありえないでしょう。これを見るにはfacesを見ます。p5.Geometryのfacesには頂点のindexの3つ組の配列が面としていくつも並んでいるので、
console.log(myCone.faces);
で出ますね。さあどうでしょう。
0: Array(3) [ 0, 5, 4 ]
1: Array(3) [ 1, 6, 5 ]
2: Array(3) [ 2, 7, 6 ]
3: Array(3) [ 3, 4, 7 ]
4: Array(3) [ 8, 9, 13 ]
5: Array(3) [ 8, 13, 12 ]
6: Array(3) [ 9, 10, 14 ]
7: Array(3) [ 9, 14, 13 ]
8: Array(3) [ 10, 11, 15 ]
9: Array(3) [ 10, 15, 14 ]
10: Array(3) [ 11, 8, 12 ]
11: Array(3) [ 11, 12, 15 ]
答え:12枚。
はぁ?
どこに12枚もあるのか謎ですから、ちょっと工夫して、次のようなコードを書きます。
const vToText = v => `(${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)})`;
for(const f of myCone.faces){
const u = myCone.vertices[f[0]];
const v = myCone.vertices[f[1]];
const w = myCone.vertices[f[2]];
console.log(`<${vToText(u)}, ${vToText(v)}, ${vToText(w)}>`);
}
結果は:
<(0.00, -50.00, 0.00), (86.60, -50.00, -50.00), (0.00, -50.00, 100.00)>
<(0.00, -50.00, 0.00), (-86.60, -50.00, -50.00), (86.60, -50.00, -50.00)>
<(0.00, -50.00, 0.00), (-0.00, -50.00, 100.00), (-86.60, -50.00, -50.00)>
※ <(0.00, -50.00, 0.00), (0.00, -50.00, 100.00), (-0.00, -50.00, 100.00)>
<(0.00, -50.00, 100.00), (86.60, -50.00, -50.00), (0.00, 50.00, 0.00)>
※ <(0.00, -50.00, 100.00), (0.00, 50.00, 0.00), (0.00, 50.00, 0.00)>
<(86.60, -50.00, -50.00), (-86.60, -50.00, -50.00), (0.00, 50.00, 0.00)>
※ <(86.60, -50.00, -50.00), (0.00, 50.00, 0.00), (0.00, 50.00, 0.00)>
<(-86.60, -50.00, -50.00), (-0.00, -50.00, 100.00), (0.00, 50.00, 0.00)>
※ <(-86.60, -50.00, -50.00), (0.00, 50.00, 0.00), (0.00, 50.00, 0.00)>
※ <(-0.00, -50.00, 100.00), (0.00, -50.00, 100.00), (0.00, 50.00, 0.00)>
※ <(-0.00, -50.00, 100.00), (0.00, 50.00, 0.00), (0.00, 50.00, 0.00)>
※がついてるのに注目してください。3つのうち2つが同じ位置になっているのが分かるでしょうか。これらの6つの三角形はつぶれています。つまり12枚ではありますが、実はそのうち6枚はダミーというか、ゴミです。描画に寄与していません。
そこで、これらの無駄な面をカットしてみましょう。フィルターを掛ければ一瞬で終わります。えいっ!
myCone.faces = myCone.faces.filter((f) => {
const u = myCone.vertices[f[0]];
const v = myCone.vertices[f[1]];
const w = myCone.vertices[f[2]];
if(u.dist(v) < 1e-9){ return false; }
if(u.dist(w) < 1e-9){ return false; }
if(v.dist(w) < 1e-9){ return false; }
return true;
});
まあ雑に。それでもっかいさっきの処理を実行すると:
<(0.00, -50.00, 0.00), (86.60, -50.00, -50.00), (0.00, -50.00, 100.00)>
<(0.00, -50.00, 0.00), (-86.60, -50.00, -50.00), (86.60, -50.00, -50.00)>
<(0.00, -50.00, 0.00), (-0.00, -50.00, 100.00), (-86.60, -50.00, -50.00)>
<(0.00, -50.00, 100.00), (86.60, -50.00, -50.00), (0.00, 50.00, 0.00)>
<(86.60, -50.00, -50.00), (-86.60, -50.00, -50.00), (0.00, 50.00, 0.00)>
<(-86.60, -50.00, -50.00), (-0.00, -50.00, 100.00), (0.00, 50.00, 0.00)>
つぶれる面が一掃されています。まあそういうことです。こんな感じで加工しなければいけないんです。現行の仕様ではね。issueを立てて仕様変更に貢献することの大切さが、少しでも伝わったなら幸いです。
見た目も確認しましょう。同じですね。
(なお、たとえば最初の消える面、3,4,7ですが、まあ4と7の「元の」頂点座標を見ればわかるんですが、確かに違う値ではあります。違う値ではありますが、まあぶっちゃけどっちも、0です。この辺詳しいことはよくわからないんですが、一つ言えることは、「p5にとっては違う点扱いなのだ」ということです。p5にとっては、違う点なんです...多分ね。)
おわりに
ここまでお読みいただいてありがとうございました。
補足:なぜgetFinalColorの仕様変更に気づいたのか?
生のシェーダーを次の記事の方法で確認したからです。
vec4 HOOK_getFinalColor(vec4 color, vec2 texCoord) { return color; }
知見は地道に積み重ねていくものです。ちょっとずついろんな気づきを丁寧に積み重ねていくと、富士登山のように、いつの間にかなんかすごい、高い所にいるものです。ハードルが高いと思っていたことがすいすいできるようになってできることの幅が広がります。それを日々楽しんでいます。なので
反応なんかどうでもいい
ですね。成長するって楽しい。



