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?

p5.jsでサイコロの回転を表現する

0
Last updated at Posted at 2026-10-12

はじめに

 サイコロの回転。

コード全文

function setup() {
  createCanvas(400,400,WEBGL);
  smooth();
  const DPR = pixelDensity();

  camera(160,160,120,0,0,0,0,0,-1);
  noStroke();
  const m_dice = new DOMMatrix();
  m_dice.translateSelf(0,0,10);
  const m_move = new DOMMatrix();

  // 行列アップデートを実行し終わったら位置を更新する
  // その都度向かいたい方向を決める
  // 以上です
  const position = createVector(0,0,10);
  const ground = createVector(0,0,-1);
  const direction = createVector(1,0,0);
  const axis = createVector(0,0,0); // ...
  const support = createVector(0,0,0); // ...

  const directionCandidate = [
    createVector(1,0,0),createVector(-1,0,0),createVector(0,1,0),createVector(0,-1,0)
  ];

  let count = 0;
  const moveDuration = 40;
  const waitDuration = 15;
  
  const confirmDirection = () => {
    const dc = []; // direction candidate.
    for(const d of directionCandidate){
      const nextPosition = position.copy().add(d.copy().mult(20));
      if(abs(nextPosition.x) > 90 || abs(nextPosition.y) > 90){ continue; }
      dc.push(d);
    }
    direction.set(random(dc));
    axis.set(direction.cross(ground));
    support.set(position).add(ground.copy().mult(10)).add(direction.copy().mult(10));
  }

  const update = () => {
    count++;
    if(count < 0){ return; }
    const prg = count/moveDuration;
    const sprg = prg*prg*(3-2*prg);
    const t = 90*sprg;
    const h = _createHinge(support, axis, t); // Hinge行列
    m_move.setMatrixValue(m_dice);
    m_move.preMultiplySelf(h); // グローバル適用
    if(count === moveDuration){
      m_dice.setMatrixValue(m_move);
      position.add(direction.copy().mult(20));
      confirmDirection();
      count = -waitDuration;
    }
  }
  confirmDirection();

  // shaderを作る。頂点彩色。
  const sh = baseMaterialShader().modify({
	  vertexDeclarations:'out vec3 vPosition;',
	  'void afterVertex': `() { vPosition = aPosition; }`,
    'fragmentDeclarations':`
	  in vec3 vPosition;
    `,
    'vec4 getFinalColor':`(vec4 color, vec2 texCoord){
      color = vec4(0.5+0.5*vPosition, 1.0);
      return color;
    }`
  });

  // 9x9のチェック柄
  const gr = createGraphics(180,180);
  gr.noStroke();
  const ctx = gr.drawingContext;
  const img = createGraphics(40/DPR,40/DPR);
  img.noStroke();
  img.background(255);
  img.fill(0);
  img.square(0,0,20/DPR);img.square(20/DPR,20/DPR,20/DPR);
  ctx.fillStyle = ctx.createPattern(img.elt, 'repeat');
  gr.rect(0,0,180,180);

  draw=()=>{

    update();

    orbitControl();
    resetShader();
    background(0);
    lights();
    texture(gr);
    plane(180);
    
    shader(sh);
    // 転置しないと駄目よ~
    _applyMatrix(m_move);
    box(19.9);
  }
}

function _createHinge(p, a, t){
  const m0 = new DOMMatrix();
  const m1 = new DOMMatrix();
  const m2 = new DOMMatrix();
  m0.translateSelf(p.x, p.y, p.z);
  m1.rotateAxisAngleSelf(a.x, a.y, a.z, t);
  m2.translateSelf(-p.x, -p.y, -p.z);
  m0.multiplySelf(m1);
  m0.multiplySelf(m2);
  return m0;
}

function _applyMatrix(m){
  applyMatrix(
    m.m11,m.m12,m.m13,m.m14,
    m.m21,m.m22,m.m23,m.m24,
    m.m31,m.m32,m.m33,m.m34,
    m.m41,m.m42,m.m43,m.m44
  );
}

image.png

材料

 DOMMatrix. mdnが提供する行列のクラス。3Dなので行列が無いと何にもできない。

カメラ

 zUpとする。すなわちz軸の正方向が上。そういうつもりでやりたいわけだが、例によってy軸がアレになっている影響でカメラ宣言がちょっとおかしなことになっているが無視しよう。

camera(160,160,120,0,0,0,0,0,-1);

noStroke(). 線は描画しない。普通描かないが、デフォルトでは描いてしまうので、面倒でも付ける。
DPR. ピクセル濃度。あとでテクスチャに使う。

必要な行列・ベクトル群

 最初にまとめて用意してしまう。あとでその都度更新する。

  const m_dice = new DOMMatrix();
  m_dice.translateSelf(0,0,10);
  const m_move = new DOMMatrix();

  // 行列アップデートを実行し終わったら位置を更新する
  // その都度向かいたい方向を決める
  // 以上です
  const position = createVector(0,0,10);
  const ground = createVector(0,0,-1);
  const direction = createVector(1,0,0);
  const axis = createVector(0,0,0); // ...
  const support = createVector(0,0,0); // ...

m_diceはサイコロのトランスフォームなんだが、移動中はm_moveを使う。これはどういうことかというと、回転時は最後にとどまっていた時のトランスフォームに回転を施す形でトランスフォームを作り、それによって描画するのだ。回転が終わったら最後にとどまっていた時のトランスフォームを更新する。位置も更新する。groundは地面...このスケッチの場合、常に一定である。direction, これはその都度選定する。今回フィールドが180x180なのでそこからはみ出ないように選んでいる。supportは回転の支点である。これを通り、axisの周りに回転する。回転の角度は90°である。

更新処理

 アップデートには行列の演算を使う。

function _createHinge(p, a, t){
  const m0 = new DOMMatrix();
  const m1 = new DOMMatrix();
  const m2 = new DOMMatrix();
  m0.translateSelf(p.x, p.y, p.z);
  m1.rotateAxisAngleSelf(a.x, a.y, a.z, t);
  m2.translateSelf(-p.x, -p.y, -p.z);
  m0.multiplySelf(m1);
  m0.multiplySelf(m2);
  return m0;
}

function _applyMatrix(m){
  applyMatrix(
    m.m11,m.m12,m.m13,m.m14,
    m.m21,m.m22,m.m23,m.m24,
    m.m31,m.m32,m.m33,m.m34,
    m.m41,m.m42,m.m43,m.m44
  );
}

createHingeは支点pを通る軸aの周りに角度tだけ回転するHinge行列を出力するもの。hingeは勝手に名付けた。蝶番という意味らしいです。内容的には平行移動して回転して平行移動するだけ。これがローカルなので、順、回転、逆の順で用意する。こうすると逆から適用されて、中心に行って、回って、また戻る形で、うまくいく。
もうひとつの_applyMatrixは行列適用である。ところで、p5には標準的な行列の枠組みが...

ありません。

applyMatrixも引数は列挙ですし、要するに行列単位での何らかのクラス、ベクトルとかそういう感じの、が、無いです。それでDOMMatrixなんかを使っています。この_applyMatrixというのはその行列を適用させるものです。ただ自分で枠組みを作っているわけではないので半分くらいは勘でやっています。まあ動けば何でもいいです。

image.png

見ればわかりますが成分の縦と横が(上記のコードと)逆になっています。もうこの辺は本当に面倒で、本当に、面倒で、混乱しないためには自分で枠組みを作ってしまうのが一番です。自分的には、行列の掛け算が数学と違った仕様になっていると頭が混乱するので、それを最優先に枠組みを組んで利用しています。ですが今回はこの枠組みでやりましょう。

仕掛けについて

 confirmDirectionで変数を決めて、updateで回転させる。これを延々と交代でやっているだけです。その際一時的に止まるように、waitを用意しています。負の範囲にある場合は実行しないわけです。その間は最後に使ったm_moveで描画するので、止まるというわけですね。

色

 頂点彩色です。aPositionをvaryingで送ってfinalColorで使っています。綺麗ですね~。これを用意した意味はいちいちトランスフォームをリセットしていない、それを確かめるためです。

テクスチャ平面

 平面ですが、チェック模様を作るのにはcreatePatternを使っています。

20x20の4つ分のところだけを作ってあとはrepeat適用です。こうすることでいくつも正方形を描く手間が省けます。難点はピクセル濃度のあたりがめんどくさくて、よくわかんなかったのでDPRで割って対処しました。createGraphicsで作ったキャンバスはDPRを考慮するので、...
そうです。

補足など

 そうですね、回転のメソッドのrotateAxisAngleSelfは角度変数に度数法を用いています。ラジアンではないため、場合によっては混乱するでしょう。今回は90°と決まっていますから、特に混乱することは無いですね。
 なお進捗ですがsmoothstepにしています。この辺はいくらでも改造できるでしょう。
 updateのここ:

    m_move.setMatrixValue(m_dice);
    m_move.preMultiplySelf(h); // グローバル適用

グローバル適用です。それにはpreの方を使います。なぜグローバルかといえば支点をグローバルで指定しているからです。結局、ローカル座標系がすってんころりんするのでローカルで支点を指定するのが不可能なんです。そこでグローバルで支点を方向と位置に基づいて決めているわけです。どう考えてもその方が楽ですからね。
 なお、ローカルヒンジに役割が無いかと言えば大間違いで、例えば折り畳みアニメーションでは大活躍します。ヒンジは楽しいです。研究のし甲斐があります。

あとは何だろ、...大きさですね、露骨に若干縮めています。いわゆるz-fightを避けるためですが、もう少し大きくてもいいかもですね。あと隙間が空くのが気になるアニメーションの場合は駄目です。まあ見栄え次第で好きにいじるところです。

  box(19.9); // ここ

アドバンストな話題

 これだけだと平面上しか移動できません。こんな風に:

image.png

乗り上げをしたい場合、空間を3次元で扱う必要があります。また、複数のサイコロをぶつからないように回転させたい場合、占有領域の管理などが必要になってきます。面倒なので、今回は回転の仕組みだけを解説しました。

おわりに

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

前回の記事

 今回こういう記事を書いたので、以前書いたサイコロの記事を振り返ってみたんですが...

 何が書いてあるのかさっぱり分かんないですね...localとかglobalの意味もいまいちだし、どうでもいいキューブマップとか使ってて本質がぼやけていて駄目ですね。結局これが1年半以上も前なので、あれから色々あって座標変換などの理解も進んだし、自サイトを作ったり、ライブラリが充実して3D周りを自由にいじれるようになって、p5のy軸問題の呪縛からも解放されて、いろいろやりやすくなったので、こういう感じで解説できるようになりました。ありがたいことです。

 行列についてあれこれ書きましたが自分のライブラリでは混乱なくやっています。p5で混乱なく行列とかそういうのをうまく扱う方法はいまだに試行錯誤です。カメラをひっくり返したり、いろんなインチキをしないといけないのは精神的に苦痛です。自分の想像ですが、p5がyひっくり返しをやっているのは「2Dと同じ感覚で!」を大昔の人が標榜して、以降ずっと継続で、今更変えようがないからです。Processingはさらに徹底しててなんと原点が左上にあります。2Dと一緒だというわけです。それで使いやすいかと言えば自分にとってはNOです。自分は2Dは2D,WebGLはWebGLと割り切っているので特に混乱しません。他にもそういう人はたくさんいるんじゃないでしょうか。しかしライブラリが正義ですからね...選択肢があればいいんですが、無いので。つまりはそういうことです。

 カリングの概念も逆になってしまいますからね。あれは「摘み取る」という意味ですが「BACKを摘み取って表だけ描画する」が完全にひっくり返って「FRONTを摘み取って表だけ描画する」なので、あれのせいでカリングの理解も遅れました。色々取り戻した今では、どうでもいいことです。

void setup() {
  size(640, 360, P3D);
  noStroke();
  fill(204);
  sphereDetail(60);
}

void draw() {
  background(0); 
  
  // Light the bottom of the sphere
  directionalLight(51, 102, 126, 0, -1, 0);
  
  // Orange light on the upper-right of the sphere
  spotLight(204, 153, 0, 360, 160, 600, 0, 0, -1, PI/2, 600); 
  
  // Moving spotlight that follows the mouse
  spotLight(102, 153, 204, 360, mouseY, 600, 0, 0, -1, PI/2, 600);

  translate(width/2, height/2, 0);
  sphere(120);
}

わざわざ中心にtranslateするの、面倒だと思うんだが...

次で100週!

 なんかさっきの記事読んでいたらまだ15週だったんですね。未熟なわけだ...
 来週で100ですが、特に企画とかは無いです。いつも通り、tipsを淡々と紹介するのみです。

さようなら

 次の100でp5と絡めた記事は最後にすることになりました。特に理由は無いのですが、物事に区切りをつけるのはとても大事なことです。最後にp5のwebglがいかに歪んでいるかをもうちょっと分かりやすく説明しますね。

function setup() {
	createCanvas(400,400,WEBGL);
	camera(400,400,400,0,0,0,0,0,1);
	noStroke();
	draw = () => {
		orbitControl();
		background(0);
		lights();
		fill('red');
		plane(400);
		fill('orange');
		translate(0,0,50);
		box(99);
	}
}

ffh55757.png

camera関数は9つの引数を取ります。最初の3つはeyeです。次の3つはcenterです。最後の3つは「上」を意味するベクトルの成分です。これが0,0,1ということはz軸の上方向を示していることになります。図を見てください。下から見上げています。

以上です。

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?