はじめに
前回の続きです。
行列サイドからも理解すると、より理解が深まるので、おすすめです。
setTransform, transform, getTransform
2次元の線形変換は行列で表現できることを前回ちらっと出したんですがちゃんと説明します。
いずれも引数は6つです。それらはどう使われるかというと、こう:
setTransformはダイレクトにそれにする。transformは右から掛け算します。行列の掛け算です。成分は、こうです。実際に確かめてみましょうか。
ctx.setTransform(1,3,6,2,10,4);
ctx.transform(0,9,5,1,-2,3);
const result = ctx.getTransform();
console.log(result.a, result.b, result.c, result.d, result.e, result.f);
getTransform()を使うと行列を取得できます。DOMMatrixというクラスになります。今必要なのはa,b,c,d,e,fのプロパティなのでそこだけ見ます。
コンソールで上記の6つの数になることを確認できます。図の方は手計算で出しました。合ってるでしょ。
行列で理解するtranslate,rotate,scale
前回説明したtranslate,rotate,scaleはすべてtransformで書き換えることができます。そして内部では行列の計算が行われています。
例によってrotateだけめんどくさいですね...これはまあ、シータがPI/2のときにですね、(1,0,1)を列ベクトルで右において、その結果が(0,1,1)になればオッケーと考えましょう。だからこれで正解です。
前回の練習の結果を行列を使って出す
前回、練習で座標系がどうなるだとか、その場合(8,15)はどこなんだとかやったと思いますが、実は座標系の計算なんかしなくてもプログラム上の計算で出せます。
ctx.fillStyle = "black";
ctx.fillRect(0,0,400,400);
ctx.fillStyle = "white";
ctx.translate(100,250);
ctx.rotate(Math.PI/2);
ctx.scale(2,3);
ctx.translate(0, -20);
ctx.beginPath();
ctx.ellipse(8,15,8,8,0,0,Math.PI*2);
ctx.fill();
// 115,266は出せるか?
const m = ctx.getTransform();
console.log(m.a,m.b,m.c,m.d,m.e,m.f); // ほぼ0, 2, -3, ほぼ0, 160, 250
const x = m.a*8 + m.c*15 + m.e*1;
const y = m.b*8 + m.d*15 + m.f*1;
console.log(x, y); // 115, 266. 出せましたね。
// transform解除
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "blue";
ctx.beginPath();
ctx.ellipse(x, y, 8,8,0,0,Math.PI*2);
ctx.fill();
ここに「ほぼ0」とあるのは三角関数が絡むことにより生じる誤差のせいで厳密に0にならないからです。特に問題はありません。図にある計算をすれば、115,266はちゃんと出ます。そしてそのx,yのところに楕円を描くと、ちゃんと重なります。ほらね:
同じ図をコピペしただけです。実際に重なるかどうかは自分で確かめてください。
応用:p5.jsで3D空間の線形変換を施した後の相対座標から絶対座標を出す
ここからは応用として、p5.jsでWebGLモードをやった場合に、rotateXやtranslateをじゃんじゃん使った場合に、そのあとの位置指定による位置が絶対座標でどうなるかみたいなことを調べる方法を紹介します。要するに、その状態でtranslate(a,b,c)とかしたとして、その場合の位置っていうのが、トランスフォームを全部リセットした場合、どう算出されるのかを調べたいわけですね。
とりあえずコードを書いてしまう。verは2.0.5です。
function setup(){
createCanvas(600, 600, WEBGL);
}
function draw(){
orbitControl();
background(0);
const m = new DOMMatrix();
lights();
translate(20,10,30);
rotateX(PI/7);
rotateY(PI/5);
scale(2,3,1);
rotateZ(PI/9);
rotate(PI/3,[1,2,3]);
translate(0,50,0);
// コピーする
// degreesかよ...
m.translateSelf(20,10,30);
m.rotateAxisAngleSelf(1,0,0,(PI/7)*180/PI);
m.rotateAxisAngleSelf(0,1,0,(PI/5)*180/PI);
m.scaleSelf(2,3,1);
m.rotateAxisAngleSelf(0,0,1,(PI/9)*180/PI);
m.rotateAxisAngleSelf(1,2,3,(PI/3)*180/PI);
m.translateSelf(0,50,0);
//console.log(m);
noStroke();
fill(0, 128, 255);
// 変換された座標系での(0,0,0)に置く
sphere(5);
// 変換された座標系での(-34,17,-61)に置く
translate(-34, 17, -61);
fill(255, 128, 0);
sphere(5);
// はいリセット
resetMatrix();
// 0,0,0の場合はこれでOK
translate(m.m41, m.m42, m.m43);
noFill();
stroke(255);
box(30);
// 一般の場合の計算方法
const x = -34*m.m11 + 17*m.m21 - 61*m.m31 + m.m41;
const y = -34*m.m12 + 17*m.m22 - 61*m.m32 + m.m42;
const z = -34*m.m13 + 17*m.m23 - 61*m.m33 + m.m43;
resetMatrix();
translate(x,y,z);
box(30);
// ほらね。
//noLoop();
}
結果:
まあオビコンでちょっと回していますが。orbitControl便利ですね...
えーと。
まず次の計算をするわけです。
translate(20,10,30);
rotateX(PI/7);
rotateY(PI/5);
scale(2,3,1);
rotateZ(PI/9);
rotate(PI/3,[1,2,3]);
translate(0,50,0);
こうしたうえで、まず普通にsphereを描画します(水色)。そのあと(-34,17,-61)だけずらしたところにsphereを描画します(オレンジ)。そのあとでトランスフォームをリセットし、まず最初の平行移動でさっきオレンジのsphereを描いたところの中心まで行ってワイヤーフレームの立方体を描きます。次にまたリセットし、今度は別の平行移動でさっき水色のsphereを描いたところの中心まで行ってワイヤーフレームの立方体を描きます。この、位置の計算方法です。
その計算のためにDOMMatrixを使います。これはjsに組み込まれている行列クラスで、3次元の線形変換を扱うことができます。これに、p5の変換と全く同じ変換を施せば、然るべき行列が出るわけですが、それを使えばさっきの座標はたちどころに計算されます。
具体的にはこうします。
対応する関数があります。translateSelf, rotateAxisAngleSelf,scaleSelfですね。これらを同じように順繰りに施していくんですが、注意点があって、rotateの角度の指定が「度数法」なんですね...ラジアンじゃない。嘘だと思うかもですが、ほんとです...そういうわけで、180/PIを掛け算します。これで全く同じ計算になります。
m.translateSelf(20,10,30);
m.rotateAxisAngleSelf(1,0,0,(PI/7)*180/PI);
m.rotateAxisAngleSelf(0,1,0,(PI/5)*180/PI);
m.scaleSelf(2,3,1);
m.rotateAxisAngleSelf(0,0,1,(PI/9)*180/PI);
m.rotateAxisAngleSelf(1,2,3,(PI/3)*180/PI);
m.translateSelf(0,50,0);
あとは掛け算するだけです。
getTransformで4x4行列を取得します。対応はこんな感じ。なので、たとえば原点であればm41,m42,m43をダイレクトに使えばいいだけです。一般の場合は、こんな風に掛け算すれば、出ます。
// はいリセット
resetMatrix();
// 0,0,0の場合はこれでOK
translate(m.m41, m.m42, m.m43);
noFill();
stroke(255);
box(30);
// 一般の場合の計算方法
const x = -34*m.m11 + 17*m.m21 - 61*m.m31 + m.m41;
const y = -34*m.m12 + 17*m.m22 - 61*m.m32 + m.m42;
const z = -34*m.m13 + 17*m.m23 - 61*m.m33 + m.m43;
resetMatrix();
translate(x,y,z);
box(30);
それで同じところに描画されます。この技術がどう使われるか分かりませんが、絶対座標が欲しい場合が、あるかもしれないので、あったら便利ですね。
おわりに
ここまでお読みいただいてありがとうございました。






