Вращение осей используя кватернионы
Не могу разобраться как вращать плоскость вместе с вращением осей. Пробовал и матрицы и углы Ейлера. Теперь остановился на кватернионах но проблема все та же: Крутишь по Х, потом по Y. Y поворачивается относительно нового поворота по оси X а вот если наоборот, сначала по Y а потом по X то ось X не меняется. Она как бы остаётся в мировых координатах. Если поменять местами перемножение то работает в обратную сторону. А как сделать что бы в обе было?
Вот видео в котором поворот идет сначала по Y, потом Х(по новой оси X) а потом опять Y(по новой оси Y). Каждый поворот повернул другую ось. Пример поворота
Это мой пример проекта. Попробуйте повернуть сначала Х а потом Y. Все работает, но если затем опять по Х то он уже не реагирует на изменения.
Ось Y я изначально повернул в сторону Z а Z в сторону Y чтобы Z смотрел вверх а Y назад.
class Quaternion {
constructor(w = 0, x = 0, y = 0, z = 0) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
}
axisAngleToQ(theta, x, y, z) {
return new Quaternion(Math.cos(theta / 2), x * Math.sin(theta / 2), y * Math.sin(theta / 2), z * Math.sin(theta / 2));
}
static multiplyQuaternions(a, b) {
const w = (a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z);
const x = (a.w * b.x + a.x * b.w - a.y * b.z + a.z * b.y);
const y = (a.w * b.y + a.x * b.z + a.y * b.w - a.z * b.x);
const z = (a.w * b.z - a.x * b.y + a.y * b.x + a.z * b.w);
return new Quaternion(w, x, y, z);
}
static conjugate(q) {
return new Quaternion(q.w, q.x * -1, q.y * -1, q.z * -1);
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
}
normalize() {
let l = this.length();
if (l === 0) {
this.x = 0;
this.y = 0;
this.z = 0;
this.w = 1;
}
else {
l = 1 / l;
this.x = this.x * l;
this.y = this.y * l;
this.z = this.z * l;
this.w = this.w * l;
}
return this;
}
}
class RotateCoords {
cvs;
ctx;
centerXInput;
centerYInput;
centerX = 250;
centerY = 250;
angleX = 0;
angleY = 0;
angleZ = 0;
angleXR = 0;
angleYR = 0;
angleZR = 0;
angleXText = undefined;
angleYText = undefined;
angleZText = undefined;
radianK = Math.PI / 180;
qX;
qXInverse;
qY;
qYInverse;
qZ;
qZInverse;
threePlaneVertices = [
//Coronal
[-100, -100, 0],
[100, -100, 0],
[-100, 100, 0],
[100, -100, 0],
[100, 100, 0],
[-100, 100, 0],
//Saggital
[0, -100, -100],
[0, -100, 100],
[0, 100, -100],
[0, -100, 100],
[0, 100, 100],
[0, 100, -100],
//Axial
[-100, 0, -100],
[100, 0, -100],
[-100, 0, 100],
[100, 0, -100],
[100, 0, 100],
[-100, 0, 100],
];
init() {
this.initCenterCoords();
this.initAngles();
this.initCanvas();
this.qX = new Quaternion().axisAngleToQ(this.angleXR, 1, 0, 0);
this.qXInverse = Quaternion.conjugate(this.qX);
this.qY = new Quaternion().axisAngleToQ(this.angleYR, 0, 0, -1);
this.qYInverse = Quaternion.conjugate(this.qY);
this.qZ = new Quaternion().axisAngleToQ(this.angleZR, 0, 1, 0);
this.qZInverse = Quaternion.conjugate(this.qZ);
}
initCenterCoords() {
window.document.getElementById("centerXInput").onchange = (e) => {
this.centerX = parseInt(e.target.value);
this.draw();
};
window.document.getElementById("centerYInput").onchange = (e) => {
this.centerY = parseInt(e.target.value);
this.draw();
};
}
initAngles() {
this.angleXText = window.document.getElementById("angleXText");
this.angleYText = window.document.getElementById("angleYText");
this.angleZText = window.document.getElementById("angleZText");
window.document.getElementById("AngleXInput").oninput = (e) => {
this.angleX = parseInt(e.target.value);
this.angleXR = this.angleX * this.radianK;
this.angleXText.textContent = this.angleX;
this.qX = new Quaternion().axisAngleToQ(this.angleXR, 1, 0, 0);
this.qXInverse = Quaternion.conjugate(this.qX);
this.draw();
}
window.document.getElementById("AngleYInput").oninput = (e) => {
this.angleY = parseInt(e.target.value);
this.angleYR = this.angleY * this.radianK;
this.angleYText.textContent = this.angleY;
this.qY = new Quaternion().axisAngleToQ(this.angleYR, 0, 0, -1);
this.qYInverse = Quaternion.conjugate(this.qY);
this.draw();
}
window.document.getElementById("AngleZInput").oninput = (e) => {
this.angleZ = parseInt(e.target.value);
this.angleZR = this.angleZ * this.radianK;
this.angleZText.textContent = this.angleZ;
this.qZ = new Quaternion().axisAngleToQ(this.angleZR, 0, 1, 0);
this.qZInverse = Quaternion.conjugate(this.qZ);
this.draw();
}
}
initCanvas() {
this.cvs = window.document.getElementById("canvas");
this.ctx = this.cvs.getContext("2d", {alpha: false});
this.ctx.fillStyle = "green";
this.ctx.strokeStyle = "green";
}
draw() {
this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
this.drawPlanes();
}
drawPlanes() {
const v = this.threePlaneVertices;
//Coronal plane
// this.ctx.fillStyle = '#008c44ca';
// this.drawTriangle(this.rotate(v[0]), this.rotate(v[1]), this.rotate(v[2]));
// this.drawTriangle(this.rotate(v[3]), this.rotate(v[4]), this.rotate(v[5]));
//Saggital plane
// this.ctx.fillStyle = '#ac3335ca';
// this.drawTriangle(this.rotate(v[6]), this.rotate(v[7]), this.rotate(v[8]));
// this.drawTriangle(this.rotate(v[9]), this.rotate(v[10]), this.rotate(v[11]));
//Axial plane
this.ctx.fillStyle = '#1c254dca';
this.drawTriangle(this.rotate(v[12]), this.rotate(v[13]), this.rotate(v[14]));
this.drawTriangle(this.rotate(v[15]), this.rotate(v[16]), this.rotate(v[17]));
}
rotate(xyz) {
const qp = new Quaternion(0, xyz[0], xyz[1], xyz[2]);
let yQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qY, qp), this.qYInverse);
let xQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qX, yQ), this.qXInverse);
let zQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qZ, xQ), this.qZInverse);
return [zQ.x, zQ.y, zQ.z];
}
drawTriangle (a, b, c) {
this.ctx.beginPath();
this.ctx.moveTo(this.centerX + a[0], this.centerY + a[1]);
this.ctx.lineTo(this.centerX + b[0], this.centerY + b[1]);
this.ctx.lineTo(this.centerX + c[0], this.centerY + c[1]);
this.ctx.fill();
}
}
window.rotateCoords = new RotateCoords();
window.rotateCoords.init();
window.rotateCoords.draw();
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#preference-ctn {
display: flex;
flex-wrap: wrap;
}
#preference-ctn div {
display: flex;
align-self: start;
max-height: 10em;
background-color: whitesmoke;
border-radius: 1em;
padding: 0.5em;
margin: 0em 0.5em;
}
#preference-ctn div label {
margin-left: 0.5em;
}
#preference-ctn div input {
margin-left: 0.3em;
margin-right: 0.8em;
}
#preference-ctn div input[type=text] {
width: 2em;
}
#setAngle-ctn span {
display: inline-block;
width: 1.3em;
}
canvas {
border: 1px solid black;
}
<section id="preference-ctn">
<div id="setCenterCoords-ctn">
<label for="centerX">Center X:</label>
<input type="text" id="centerXInput" name="centerX" value="250">
<label for="centerY">Center Y:</label>
<input type="text" id="centerYInput" name="centerY" value="250">
</div>
<div id="setAngle-ctn">
<label for="AngleX">Angle X: <span id="angleXText">0</span></label>
<input type="range" id="AngleXInput" name="CoordsX" min="-90" value="0" max="90">
<label for="AngleY">Angle Y: <span id="angleYText">0</span></label>
<input type="range" id="AngleYInput" name="CoordsY" min="-90" value="0" max="90">
<label for="AngleZ">Angle Z: <span id="angleZText">0</span></label>
<input type="range" id="AngleZInput" name="CoordsZ" min="-90" value="0" max="90">
</div>
</section>
<h3>Canvas 500x500</h3>
<div id="canvas-ctn">
<canvas id="canvas" width="500" height="500"></canvas>
</div>
Вот код поворотов
const qp = new Quaternion(0, xyz[0], xyz[1], xyz[2]);
let yQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qY, qp), this.qYInverse);
let xQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qX, yQ), this.qXInverse);
let zQ = Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(this.qZ, xQ), this.qZInverse);
return [zQ.x, zQ.y, zQ.z];
По этой формуле: p' = qpq−1
Ответы (1 шт):
Я подогнал ваш код под видео. Поверните первый угол на 30 градусов, второй на 30 градусов, третий на -30 градусов. Получится последовательность вращений напоминающая вращения на видео. Ортогональная проекция изменена так что бы начальное положение плоскости на экране было близким к видео. Повороты переименованы - это не повороты вокруг трёх разных осей. Повороты применяются к точкам в обратном порядке - это важно.
Повороты делаются вокруг осей ZXZ. Думаю что нужно поменять систему координат - вероятно, должно быть XYX. Для этого все оси надо переставить местами: X -> Y, Y -> Z, Z -> X. Должно быть не сложно, но надо будет поменять координаты всех точек, направляющие косинусы в поворотах и проекцию точек на экран. Оставляю это сделать вам самому.
class Quaternion {
constructor(w = 0, x = 0, y = 0, z = 0) {
this.w = w;
this.x = x;
this.y = y;
this.z = z;
}
axisAngleToQ(theta, x, y, z) {
return new Quaternion(Math.cos(theta / 2), x * Math.sin(theta / 2), y * Math.sin(theta / 2), z * Math.sin(theta / 2));
}
static multiplyQuaternions(a, b) {
const w = (a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z);
const x = (a.w * b.x + a.x * b.w - a.y * b.z + a.z * b.y);
const y = (a.w * b.y + a.x * b.z + a.y * b.w - a.z * b.x);
const z = (a.w * b.z - a.x * b.y + a.y * b.x + a.z * b.w);
return new Quaternion(w, x, y, z);
}
static conjugate(q) {
return new Quaternion(q.w, q.x * -1, q.y * -1, q.z * -1);
}
length() {
return Math.sqrt(this.x * this.x + this.y * this.y + this.z * this.z + this.w * this.w);
}
normalize() {
let l = this.length();
if (l === 0) {
this.x = 0;
this.y = 0;
this.z = 0;
this.w = 1;
}
else {
l = 1 / l;
this.x = this.x * l;
this.y = this.y * l;
this.z = this.z * l;
this.w = this.w * l;
}
return this;
}
}
class RotateCoords {
cvs;
ctx;
centerX = 250;
centerY = 250;
angle1 = 0;
angle2 = 0;
angle3 = 0;
threePlaneVertices = [
//Coronal
[-100, -100, 0],
[100, -100, 0],
[-100, 100, 0],
[100, -100, 0],
[100, 100, 0],
[-100, 100, 0],
//Saggital
[0, -100, -100],
[0, -100, 100],
[0, 100, -100],
[0, -100, 100],
[0, 100, 100],
[0, 100, -100],
//Axial
[-100, 0, -100],
[100, 0, -100],
[-100, 0, 100],
[100, 0, -100],
[100, 0, 100],
[-100, 0, 100],
];
init() {
this.initCenterCoords();
this.initAngles();
this.initCanvas();
}
initCenterCoords() {
document.getElementById("centerXInput").onchange = (e) => {
this.centerX = parseInt(e.target.value);
this.draw();
};
document.getElementById("centerYInput").onchange = (e) => {
this.centerY = parseInt(e.target.value);
this.draw();
};
}
initAngles() {
document.getElementById("Angle1Input").oninput = (e) => {
this.angle1 = parseInt(e.target.value);
document.getElementById("Angle1Text").textContent = this.angle1;
this.draw();
}
document.getElementById("Angle2Input").oninput = (e) => {
this.angle2 = parseInt(e.target.value);
document.getElementById("Angle2Text").textContent = this.angle2;
this.draw();
}
document.getElementById("Angle3Input").oninput = (e) => {
this.angle3 = parseInt(e.target.value);
document.getElementById("Angle3Text").textContent = this.angle3;
this.draw();
}
}
initCanvas() {
this.cvs = document.getElementById("canvas");
this.ctx = this.cvs.getContext("2d", {alpha: false});
this.ctx.fillStyle = "green";
this.ctx.strokeStyle = "green";
}
draw() {
this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height);
this.drawPlanes();
}
drawPlanes() {
const v = this.threePlaneVertices;
//Coronal plane
// this.ctx.fillStyle = '#008c44ca';
// this.drawTriangle(v[0], v[1], v[2]);
// this.drawTriangle(v[3], v[4], v[5]);
//Saggital plane
// this.ctx.fillStyle = '#ac3335ca';
// this.drawTriangle(v[6], v[7], v[8]);
// this.drawTriangle(v[9], v[10], v[11]);
//Axial plane
this.ctx.fillStyle = '#1c254dca';
this.drawTriangle(v[12], v[13], v[14]);
this.drawTriangle(v[15], v[16], v[17]);
}
rotate(xyz) {
const conjugate = (p, q) => Quaternion.multiplyQuaternions(Quaternion.multiplyQuaternions(q, p), Quaternion.conjugate(q));
const rotate = (qp, angle, x, y, z) =>
qp = conjugate(qp, new Quaternion().axisAngleToQ(angle * Math.PI / 180, x, y, z));
let qp = new Quaternion(0, xyz[0], xyz[1], xyz[2]);
qp = rotate(qp, this.angle3, 0, 0, 1);
qp = rotate(qp, this.angle2, 1, 0, 0);
qp = rotate(qp, this.angle1, 0, 0, 1);
return [qp.x, qp.y, qp.z];
}
project(p) {
return [this.centerX + p[0] - 0.2 * p[2], this.centerY + p[1] + 0.4 * p[2]]
}
drawTriangle (a, b, c) {
this.ctx.beginPath();
this.ctx.moveTo(...this.project(this.rotate(a)));
this.ctx.lineTo(...this.project(this.rotate(b)));
this.ctx.lineTo(...this.project(this.rotate(c)));
this.ctx.fill();
}
}
rotateCoords = new RotateCoords();
rotateCoords.init();
rotateCoords.draw();
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
#preference-ctn {
display: flex;
flex-wrap: wrap;
}
#preference-ctn div {
display: flex;
align-self: start;
max-height: 10em;
background-color: whitesmoke;
border-radius: 1em;
padding: 0.5em;
margin: 0em 0.5em;
}
#preference-ctn div label {
margin-left: 0.5em;
}
#preference-ctn div input {
margin-left: 0.3em;
margin-right: 0.8em;
}
#preference-ctn div input[type=text] {
width: 2em;
}
#setAngle-ctn span {
display: inline-block;
width: 1.3em;
}
canvas {
border: 1px solid black;
}
<section id="preference-ctn">
<div id="setCenterCoords-ctn">
<label for="centerX">Center X:</label>
<input type="text" id="centerXInput" name="centerX" value="250">
<label for="centerY">Center Y:</label>
<input type="text" id="centerYInput" name="centerY" value="250">
</div>
<div id="setAngle-ctn">
<label for="Angle1">Angle 1: <span id="Angle1Text">0</span></label>
<input type="range" id="Angle1Input" name="Coords1" min="-90" value="0" max="90">
<label for="Angle2">Angle 2: <span id="Angle2Text">0</span></label>
<input type="range" id="Angle2Input" name="Coords2" min="-90" value="0" max="90">
<label for="Angle3">Angle 3: <span id="Angle3Text">0</span></label>
<input type="range" id="Angle3Input" name="Coords3" min="-90" value="0" max="90">
</div>
</section>
<h3>Canvas 500x500</h3>
<div id="canvas-ctn">
<canvas id="canvas" width="500" height="500"></canvas>
</div>