Вращение осей используя кватернионы

Не могу разобраться как вращать плоскость вместе с вращением осей. Пробовал и матрицы и углы Ейлера. Теперь остановился на кватернионах но проблема все та же: Крутишь по Х, потом по 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 шт):

Автор решения: Stanislav Volodarskiy

Я подогнал ваш код под видео. Поверните первый угол на 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>

→ Ссылка