0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

2Dと3Dのcanvasのtransformを行列で理解してついでにp5.jsに応用する

Posted at

はじめに

 前回の続きです。

行列サイドからも理解すると、より理解が深まるので、おすすめです。

setTransform, transform, getTransform

 2次元の線形変換は行列で表現できることを前回ちらっと出したんですがちゃんと説明します。

いずれも引数は6つです。それらはどう使われるかというと、こう:

説明11.png

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で書き換えることができます。そして内部では行列の計算が行われています。

説明12.png

例によってrotateだけめんどくさいですね...これはまあ、シータがPI/2のときにですね、(1,0,1)を列ベクトルで右において、その結果が(0,1,1)になればオッケーと考えましょう。だからこれで正解です。

前回の練習の結果を行列を使って出す

 前回、練習で座標系がどうなるだとか、その場合(8,15)はどこなんだとかやったと思いますが、実は座標系の計算なんかしなくてもプログラム上の計算で出せます。

説明13.png

  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のところに楕円を描くと、ちゃんと重なります。ほらね:

erefwrwrwr.png

同じ図をコピペしただけです。実際に重なるかどうかは自分で確かめてください。

応用: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();
}

結果:

wrrwfwfw.png

まあオビコンでちょっと回していますが。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の変換と全く同じ変換を施せば、然るべき行列が出るわけですが、それを使えばさっきの座標はたちどころに計算されます。

 具体的にはこうします。

説明14.png

対応する関数があります。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);

あとは掛け算するだけです。

説明15.png

 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);

それで同じところに描画されます。この技術がどう使われるか分かりませんが、絶対座標が欲しい場合が、あるかもしれないので、あったら便利ですね。

おわりに

 ここまでお読みいただいてありがとうございました。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?