マイクラのためのお数学
最終更新: 2024/10/16
はじめに
この記事はコマンドの要素をほぼ含みません。それだけはじめにお伝えしておきます。
(どちらかというとMODとかプラグインとかScriptAPIとか書く人向け)
この記事はおそらく数学力が落ちているであろう未来の私自身のために、数学苦手マンの私が捧ぐマイクラのための数学知識を書き殴る記事です。
ネットに転がってそうで転がってない情報を置きます。
メモに近いです
数学は苦手なので間違ったこと言ってたらコメントでお願いします、、、
ヨー角・ピッチ角から三次元単位ベクトルをつくる
このコードになる理由は説明するものではないと思うのでコピペでどうぞ
public Vector3 getDirection(Rotation rotation) {
final float yaw = rotation.yaw();
final float pitch = rotation.pitch();
return new Vector3(
-Math.sin(yaw * Math.PI / 180) * Math.cos(pitch * Math.PI / 180),
-Math.sin(pitch * Math.PI / 180),
Math.cos(yaw * Math.PI / 180) * Math.cos(pitch * Math.PI / 180)
);
}
yawがヨー角、pitchがピッチ角、三次元ベクトルの成分は上からx(東西), y(上下), z(南北)です
RotationとVector3の中身は割愛
三次元ベクトルからヨー角とピッチ角による回転を得る
上のやつの逆です
こっちは解説できなくはないですがマイクラでしか使えない知識ですし正直コピペでいいと思います
public Rotation getRotation(Vector3 vector) {
final double x = vector.x();
final double y = vector.y();
final double z = vector.z();
final double length = Math.sqrt(x * x + y * y + z * z);
return new Rotation(
(float) (-Math.atan2(x / length, z / length) * 180d / Math.PI),
(float) (-Math.asin(y / length) * 180d / Math.PI)
);
}
回転の成分は上からヨー角、ピッチ角です
x, y, zもさっきと同じです
プレイヤーの視線に垂直な2つのベクトルを求める
コマンドで言うところのローカル座標(^ ^ ^)です
プレイヤーの視線の向きと同じ向きの単位ベクトルをvとします
求める順番が大切で、まずプレイヤーから見て左(右)向きのベクトルを求める必要があります
public Vector3 getLocalX(Vector3 v) {
final double length = v.x() * v.x() + v.z() * v.z();
return new Vector3(
v.z() / length,
0,
-v.x() / length
);
}
x成分とz成分を入れ替えて、左向きにするためにx成分だったほうの符号を逆にします
あとは単位ベクトルが欲しいので各成分を長さで割っておきます
短縮するとvec3(z, 0, -x).normalize()
左向きベクトルが出せたら上向きベクトルも求めときます
先にこれ作っときます
public Vector3 cross(Vetcor3 a, Vector3 b) {
final double x1 = a.x();
final double y1 = a.y();
final double z1 = a.z();
final double x2 = b.x();
final double y2 = b.y();
final double z2 = b.z();
return new Vector3Builder(
y1 * z2 - z1 * y2,
z1 * x2 - x1 * z2,
x1 * y2 - y1 * x2
);
}
そしたらこれで終わりです
public Vector3 getLocalY(Vector3 v) {
return cross(v, getLocalX(v));
}
正面向きベクトルと左向きベクトルの外積を求めてる感じです
ロール角の概念を勝手につくる
正面向きベクトルに対する左向き、上向きベクトルをロール角を指定して横に任意の大きさだけ回転することをゴールとします
まずロドリゲスの回転公式と呼ばれるものを使って左向きベクトルを正面向きベクトルを軸に回転します
public Vector3 rotate(Vector3 target, Vector3 axis, float degree) {
final double angle = degree * Math.PI / 180;
final double sin = Math.sin(angle);
final double cos = Math.cos(angle);
final double x = axis.x();
final double y = axis.y();
final double z = axis.z();
final double[][] matrix = new double[][]{
new double[]{
cos + x * x * (1 - cos),
x * y * (1 - cos) - z * sin,
x * z * (1 - cos) + y * sin
},
new double[]{
y * x * (1 - cos) + z * sin,
cos + y * y * (1 - cos),
y * z * (1 - cos) - x * sin
},
new double[]{
z * x * (1 - cos) - y * sin,
z * y * (1 - cos) + x * sin,
cos + z * z * (1 - cos)
}
};
final double tx = target.x();
final double ty = target.y();
final double tz = target.z();
return new Vector3(
matrix[0][0] * tx + matrix[0][1] * ty + matrix[0][2] * tz;
matrix[1][0] * tx + matrix[1][1] * ty + matrix[1][2] * tz;
matrix[2][0] * tx + matrix[2][1] * ty + matrix[2][2] * tz;
);
}
この関数を用意した前提で、
final Vector3 rotatedLeft = rotate(getLocalX(v), v, degree);
こうします
getLocalXとかvとかはさっきのやつです
degreeはロール角です
そしたら残るは上向きベクトルの回転ですが、これは簡単というかさっきと同じことすれば終わりです
final Vector3 rotatedUp = cross(v, rotatedLeft);
正面向きベクトルと左向きベクトルの外積ですね
傾きうる直方体範囲の内部に任意の点があるかを確かめる
これは簡単で、6つの面それぞれにおいて、その点が面の表にあるか裏にあるかを確かめればいいです
全部の面で裏にある判定なら内部ですね
その判定にはベクトルの内積が使えます
直方体の向きをオイラー角で表します
直方体の中心から見て、
左右に伸びる辺の両端を結ぶ左向きベクトルをbx
上下に伸びる辺の両端を結ぶ上向きベクトルをby
前後に伸びる辺の両端を結ぶ前方向きベクトルをbz
とすると、
public boolean isInside(BoundingBox box, Vector3 point) {
// 直方体の中心から伸ばしたいので長さを半分にする
final Vector3 cx = bx.scale(0.5);
final Vector3 cy = by.scale(0.5);
final Vector3 cz = bz.scale(0.5);
// 直方体の中心からそれぞれの面の中心に伸びるベクトル
final Set<Vector3> locations = Set.of(
center().add(cz), // forward
center().subtract(cz), // back
center().add(cx), // left
center().subtract(cx), // right
center().add(cy), // up
center().subtract(cy) // down
);
for (final Vector3 location : locations) {
final Vector3 directionToCenter = location.getDirectionTo(center);
final Vector3 directionToPoint = location.getDirectionTo(point);
// 内積が負であればその点は面の表側(直方体の外部)
if (directionToCenter.dot(directionToPoint) < 0) {
return false;
}
}
// すべての内積が正だったら真を返す
return true;
}
getDirectionTo()はベクトルの引き算して正規化してるだけです
dot()は内積求めてるだけです
一応getDirectionTo()だけ書いとくとこう
public Vector3 getDirectionTo(Vector3 other) {
return other.subtract(this).normalize();
}
thisはVector3です
2つの直方体の衝突判定も書こうかな、と思いましたが分離軸定理について長々と解説しなきゃならんのでまたいつか
図形の描画(星型)
任意のベクトルを正面向きベクトルとしたときの左向きベクトルと上向きベクトルが求められるならば三次元空間上に任意方向の正円を描画できるはずです:
public void drawCircle(Vector3 center, Vector3 direction, float radius) {
for (int i = 0; i < 360; i++) {
dot(center.add(
getPointOnAngle(direction, i, radius)
));
}
}
public Vector3 getPointOnCircle(Vector3 direction, float degree, float radius) {
final double radian = degree * Math.PI / 180;
return getLocalX(v).scale(Math.cos(rad))
.add(
getLocalY(v).scale(Math.sin(rad))
);
}
dotは座標に点を打つ関数、radiusは円の半径、centerは円の中心、directionは円の方向となる単位ベクトルです
星を描くには、まず整数n, kを作ります
この値を変えるといろんな図形になります
五芒星の場合はnを5に、七芒星なら7にします
kは何を描きたいかによって変わりますが、五芒星ならkは2です
int n = 5;
int k = 2;
図形の頂点の座標が入る配列を作り、円の角度をn等分して円周上の座標を入れます
つまり図形の頂点の座標を全部求めて配列にぶち込みます
int size = 3; // 図形のサイズ
final Vector3[] points = new Vector3[n];
for (int i = 0; i < n; i++) {
points[i] = getPointOnCircle(v, (360f / n) * i + 90, size);
}
このまま各点を線で結んでも多角形になるだけなのでソートします
final Vector3[] sortedPoints = new Vector3[n];
int index = k;
for (int i = 0; i < points.length; i++) {
index += k;
if (index >= n) index -= n;
sortedPoints[i] = points[index];
}
何やってるのかというと、頂点一つ一つに番号を振って、n個の頂点をk個おきに取り出して新しい配列に入れて、kがnを超えたらその超過分をそのまま番号にして、、っていう並べ替えをしてるだけです
よくわかんなければn芒星, n, kあたりの単語を入れて検索すれば多分でてきます
あとは順番通りに線で結んで終わりです
final Vector3 center = new Vector3(1, 2, 3); // 図形の中心にする座標
for (int i = 0; i < sortedPoints.length; i++) {
final Vector3 start = sortedPoints[i];
final Vector3 end = (i == sortedPoints.length - 1)
? sortedPoints[0]
: sortedPoints[i + 1];
line(start.add(center), end.add(center));
}
line()は始点と終点を指定して線を引く関数です
中身は割愛します
チャンクの端の座標
ベクトルから離れますが、ある座標が存在するチャンクの北西の端の座標は、ある座標をpとすると
Vector3 p;
final Vector3 northWest = new Vector3(
Math.floor(p.x() / 16) * 16,
p.y(),
Math.floor(p.z() / 16) * 16
);
で求められます
floor(各成分 ÷ 16) × 16ですね
知ってて損はないです
ホーミングの挙動
紹介する価値があるのかすら疑問ですが、端的に言えばベクトルの足し算するだけです
ターゲットの座標をt, 飛翔体の座標をs, 飛翔体の現在の向きを表す単位ベクトルをrとすると
Vector3 r;
Vector3 s;
Vector3 t;
final Vector3 dirToTarget = t.subtract(s).normalize();
final Vector3 direction = s.add(r).add(dirToTarget).subtract(s);
directionが求めるべき飛翔体の新しい向きです
あたりまえの話ですが、
ベクトルrの長さを小さくするとより早くターゲットのほうを向きます
ベクトルdirToTargetの長さを小さくするとターゲットのほうを向くのが遅くなります
ディスプレイエンティティの回転をオイラー角で
ディスプレイエンティティの回転は基本的にクォータニオンによる四元数形式か、軸ベクトルと弧度による軸角度形式でのみ操作できます
四元数はあまりに直感的でないので軸角度形式を使いたいです
ただ軸角度形式も順番を間違えると正しくオイラー角から変換できません
以下が正しく変換できる回転順序の例です:
正面向きベクトルを軸としてロール角で回転
左向きベクトルを軸としてピッチ角で回転
絶対座標における真上向きベクトルを軸として-(ヨー角 + 90)で回転
コードに起こすとこうなります
public void rotateQuaternion(Quaternionf quaternion, Vector3 axis, float degree) {
quaternion.rotateAxis(
(float) (degree * Math.PI / 180),
(float) axis.x(),
(float) axis.y(),
(float) axis.z()
);
}
public Quaternionf getQuaternion(float yaw, float pitch, float roll) {
final Quaterionf q = new Quaternionf();
final Vector3 forward = getDirection(new Rotation(
yaw, pitch
));
rotateQuaternion(q, forward, roll);
rotateQuaternion(q, getLocalX(forward), pitch);
rotateQuaternion(q, new Vector3(0, 1, 0), -(yaw + 90));
return q;
}
ヨー角に90度足してから符号を逆にするのはMinecraftとの帳尻合わせのためなので深い意味は特にありません
Quaternionfはorg.jomlのやつです
おわりに
少ないのであとなんか思いついたら増やします
数学は苦手なのでなんかリクエストされても作れない可能性が高いです
悪しからず…