From 78de2e31b66e15ca777791ee82c0c666fea67928 Mon Sep 17 00:00:00 2001
From: Jamie Temple <jamie-temple@live.de>
Date: Wed, 6 Jul 2022 14:30:53 +0200
Subject: [PATCH] feat: multi slerp

---
 src/02-quaternion/Axes.ts           |  32 ++++
 src/02-quaternion/DemoQuaternion.ts |  27 ++-
 src/02-quaternion/Quaternion.ts     | 245 ++++++++++++++--------------
 src/02-quaternion/Rotator.ts        | 116 +++++++++++++
 4 files changed, 300 insertions(+), 120 deletions(-)
 create mode 100644 src/02-quaternion/Axes.ts
 create mode 100644 src/02-quaternion/Rotator.ts

diff --git a/src/02-quaternion/Axes.ts b/src/02-quaternion/Axes.ts
new file mode 100644
index 0000000..9ead753
--- /dev/null
+++ b/src/02-quaternion/Axes.ts
@@ -0,0 +1,32 @@
+import * as THREE from 'three';
+import { PipelineRenderable } from '../core/Pipeline';
+import { Line1d } from "../core/Shapes";
+
+class Axes implements PipelineRenderable {
+    public x: Line1d;
+    public y: Line1d;
+    public z: Line1d;
+    private _objects: THREE.Group;
+
+    constructor(max: number) {
+        this._objects = new THREE.Group();
+        this.x = new Line1d([new THREE.Vector3(0, 0, 0), new THREE.Vector3(max, 0, 0)], new THREE.Color(0xff0000));
+        this.y = new Line1d([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, max, 0)], new THREE.Color(0x00ff00));
+        this.z = new Line1d([new THREE.Vector3(0, 0, 0), new THREE.Vector3(0, 0, max)], new THREE.Color(0x0000ff));
+        this._objects.add(this.x.object());
+        this._objects.add(this.y.object());
+        this._objects.add(this.z.object());
+        const offset = 0.001;
+        this.x.position().z = offset;
+        this.y.position().z = offset;
+        this.z.position().z = offset;
+    }
+    object(): THREE.Object3D {
+        return this._objects;
+    }
+    position(): THREE.Vector3 {
+        return this._objects.position;
+    }
+}
+
+export { Axes };
\ No newline at end of file
diff --git a/src/02-quaternion/DemoQuaternion.ts b/src/02-quaternion/DemoQuaternion.ts
index e8be8fd..6424c96 100644
--- a/src/02-quaternion/DemoQuaternion.ts
+++ b/src/02-quaternion/DemoQuaternion.ts
@@ -1,10 +1,35 @@
+import * as THREE from 'three';
 import { PipelineData } from "../core/Pipeline";
+import { Mesh } from "../core/Shapes";
+import { Axes } from './Axes';
+import { Rotator } from './Rotator';
 
 class QuaternionDemo extends PipelineData {
     public constructor() { super(); }
 
     data(): void {
-        /* INIT DATA HERE */
+        const grid = new THREE.GridHelper(10, 10);
+        this.scene.add(grid);
+
+        const light = new THREE.DirectionalLight(0x8e8e8e, 1);
+        light.position.set(1, 1, 1);
+        this.scene.add(light);
+
+        const pointLight = new THREE.PointLight(0x6e6e8e, 1);
+        pointLight.position.set(-1, -1, -1);
+        this.scene.add(pointLight);
+
+        const axes = new Axes(5);
+        this.addObject(axes);
+
+        const material = new THREE.MeshLambertMaterial({ color: 0xffffff });
+        const rotationObject: Mesh = new Mesh("../../assets/suzanne.obj", new THREE.Color(0xffffff));
+        rotationObject.material = material;
+        this.addObject(rotationObject);
+
+        const rotator: Rotator = new Rotator(rotationObject.object());
+        this.addObserver(rotator);
+        this.addGUI(rotator);
     }
 }
 
diff --git a/src/02-quaternion/Quaternion.ts b/src/02-quaternion/Quaternion.ts
index 4c116f9..89397c8 100644
--- a/src/02-quaternion/Quaternion.ts
+++ b/src/02-quaternion/Quaternion.ts
@@ -1,171 +1,178 @@
+import * as THREE from 'three';
 export class Quaternion {
-    public x: number;
-    public y: number;
-    public z: number;
-    public w: number;
+    private _x: number;
+    private _y: number;
+    private _z: number;
+    private _w: number;
 
     constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1) {
-        this.x = x;
-        this.y = y;
-        this.z = z;
-        this.w = w;
+        this._x = x;
+        this._y = y;
+        this._z = z;
+        this._w = w;
     }
 
-    public static fromEuler(x: number, y: number, z: number): Quaternion {
-        const c1 = Math.cos(x / 2);
-        const s1 = Math.sin(x / 2);
-        const c2 = Math.cos(y / 2);
-        const s2 = Math.sin(y / 2);
-        const c3 = Math.cos(z / 2);
-        const s3 = Math.sin(z / 2);
+    public get x(): number { return this._x; }
+    public get y(): number { return this._y; }
+    public get z(): number { return this._z; }
+    public get w(): number { return this._w; }
 
-        const qw = c1 * c2 * c3 + s1 * s2 * s3;
-        const qx = s1 * c2 * c3 - c1 * s2 * s3;
-        const qy = c1 * s2 * c3 + s1 * c2 * s3;
-        const qz = c1 * c2 * s3 - s1 * s2 * c3;
-
-        return new Quaternion(qx, qy, qz, qw);
+    public set x(value: number) { 
+        this._x = value;
     }
 
-    public static toEulerMatrix(q: Quaternion): Array<Array<number>> {
-
-        let container = new Array<Array<number>>();
-
-        let row1 = new Array<number>();
-        row1.push(1 - 2 * (q.y * q.y + q.z * q.z));
-        row1.push(2 * (q.x * q.y - q.z * q.w));
-        row1.push(2 * (q.x * q.z + q.y * q.w));
-        container.push(row1);
-
-        let row2 = new Array<number>();
-        row2.push(2 * (q.x * q.y + q.z * q.w));
-        row2.push(1 - 2 * (q.x * q.x + q.z * q.z));
-        row2.push(2 * (q.y * q.z - q.x * q.w));
-        container.push(row2);
+    public set y(value: number) {
+        this._y = value;
+    }
 
-        let row3 = new Array<number>();
-        row3.push(2 * (q.x * q.z - q.y * q.w));
-        row3.push(2 * (q.y * q.z + q.x * q.w));
-        row3.push(1 - 2 * (q.x * q.x + q.y * q.y));
-        container.push(row3);
+    public set z(value: number) { 
+        this._z = value;
+    }
 
-        return container;
-    }        
+    public set w(value: number) {
+        this._w = value;
+    }
 
-    public static identity(): Quaternion {
-        return new Quaternion(0, 0, 0, 1);
+    public get eulerMatrix(): THREE.Matrix4 {
+        const copy = Quaternion.normalize(this.clone());
+        const m = new THREE.Matrix4();
+        m.set(
+            1 - 2 * (copy._y * copy._y + copy._z * copy._z), 2 * (copy._x * copy._y - copy._z * copy._w), 2 * (copy._x * copy._z + copy._y * copy._w), 0,
+            2 * (copy._x * copy._y + copy._z * copy._w), 1 - 2 * (copy._x * copy._x + copy._z * copy._z), 2 * (copy._y * copy._z - copy._x * copy._w), 0,
+            2 * (copy._x * copy._z - copy._y * copy._w), 2 * (copy._y * copy._z + copy._x * copy._w), 1 - 2 * (copy._x * copy._x + copy._y * copy._y), 0,
+            0, 0, 0, 1
+        );
+        return m;
     }
 
-    public static lerp(qa: Quaternion, qb: Quaternion, t: number): Quaternion {
-        return qb.clone().sub(qa).multiplyScalar(t).add(qa);
+    public get array(): number[] {
+        return [this._x, this._y, this._z, this._w];
     }
 
-    public static slerp(qa: Quaternion, qb: Quaternion, t: number): Quaternion {
-        let dot = qa.dot(qb);
+    public fromEuler(euler: THREE.Vector3): this {
+        const c1 = Math.cos(euler.x / 2);
+        const s1 = Math.sin(euler.x / 2);
+        const c2 = Math.cos(euler.y / 2);
+        const s2 = Math.sin(euler.y / 2);
+        const c3 = Math.cos(euler.z / 2);
+        const s3 = Math.sin(euler.z / 2);
 
-        if (dot < 0.0)
-        {
-            qb.multiplyScalar(-1);
-            dot = -dot;
-        }
-        
-        if (dot > 0.999999)
-        {
-            return Quaternion.slerp(qa, qb, t).normalise();
-        }
+        this._x = s1 * c2 * c3 + c1 * s2 * s3;
+        this._y = c1 * s2 * c3 - s1 * c2 * s3;
+        this._z = c1 * c2 * s3 + s1 * s2 * c3;
+        this._w = c1 * c2 * c3 - s1 * s2 * s3;
 
-        const theta0 = Math.acos(dot);
-        const theta = theta0 * t;
-        const sinTheta = Math.sin(theta);
-        const sinTheta0 = Math.sin(theta0);
-        const s0 = Math.cos(theta) - dot * sinTheta / sinTheta0;
-        const s1 = sinTheta / sinTheta0;
+        this.copy(Quaternion.normalize(this));
 
-        return qa.clone().multiplyScalar(s0).add(qb.clone().multiplyScalar(s1));
+        return this;
     }
 
     public toString(): string {
-        return `Quaternion(${this.x}, ${this.y}, ${this.z}, ${this.w})`;
+        return `Quaternion(${this._x}, ${this._y}, ${this._z}, ${this._w})`;
     }
 
     public set(x: number, y: number, z: number, w: number): this {
-        this.x = x;
-        this.y = y;
-        this.z = z;
-        this.w = w;
+        this._x = x;
+        this._y = y;
+        this._z = z;
+        this._w = w;
         return this;
     }
 
     public copy(q: Quaternion): this {
-        this.x = q.x;
-        this.y = q.y;
-        this.z = q.z;
-        this.w = q.w;
+        this._x = q._x;
+        this._y = q._y;
+        this._z = q._z;
+        this._w = q._w;
         return this;
     }
 
     public clone(): Quaternion {
-        return new Quaternion(this.x, this.y, this.z, this.w);
+        return new Quaternion(this._x, this._y, this._z, this._w);
     }
 
-    public add(q: Quaternion): this {
-        this.x += q.x;
-        this.y += q.y;
-        this.z += q.z;
-        this.w += q.w;
-        return this;
+
+    public static identity(): Quaternion {
+        return new Quaternion(0, 0, 0, 1);
     }
 
-    public sub(q: Quaternion): this {
-        this.x -= q.x;
-        this.y -= q.y;
-        this.z -= q.z;
-        this.w -= q.w;
-        return this;
+    public static slerp(qa: Quaternion, qb: Quaternion, t: number): Quaternion {
+
+        let cq1 = qa.clone();
+        let cq2 = qb.clone();
+
+        cq1 = Quaternion.normalize(cq1);
+        cq2 = Quaternion.normalize(cq2);
+
+        let costheta = Quaternion.dot(cq1, cq2);
+
+        if (costheta < 0.0) {
+            cq2 = Quaternion.inverse(cq2);
+            costheta = -costheta;
+        }
+
+        if (costheta > 0.9995) {
+            return Quaternion.add(cq1, Quaternion.multiplyScalar(
+                Quaternion.subtract(cq2, cq1), t));
+        }
+
+        const theta = Math.acos(costheta);
+        const thetat = theta * t;
+
+        const sinthetat = Math.sin(thetat);
+        const sintheta = Math.sin(theta);
+        const s0 = Math.cos(theta) - costheta * sinthetat / sintheta;
+        const s1 = sinthetat / sintheta;
+
+        return Quaternion.add(
+            Quaternion.multiplyScalar(cq1, s0),
+            Quaternion.multiplyScalar(cq2, s1)
+        )
     }
 
-    public multiply(q: Quaternion): this {
-        const x = this.x;
-        const y = this.y;
-        const z = this.z;
-        const w = this.w;
+    public static add(qa: Quaternion, qb: Quaternion): Quaternion {
+        return new Quaternion(qa._x + qb._x, qa._y + qb._y, qa._z + qb._z, qa._w + qb._w);
+    }
 
-        this.x = w * q.x + x * q.w + y * q.z - z * q.y;
-        this.y = w * q.y + y * q.w + z * q.x - x * q.z;
-        this.z = w * q.z + z * q.w + x * q.y - y * q.x;
-        this.w = w * q.w - x * q.x - y * q.y - z * q.z;
-        return this;
+    public static subtract(qa: Quaternion, qb: Quaternion): Quaternion {
+        return new Quaternion(qa._x - qb._x, qa._y - qb._y, qa._z - qb._z, qa._w - qb._w);
     }
 
-    public multiplyScalar(s: number): this {
-        this.x *= s;
-        this.y *= s;
-        this.z *= s;
-        this.w *= s;
-        return this;
+    public static multiply(qa: Quaternion, qb: Quaternion): Quaternion {
+        return new Quaternion(
+            qa._w * qb._x + qa._x * qb._w + qa._y * qb._z - qa._z * qb._y,
+            qa._w * qb._y + qa._y * qb._w + qa._z * qb._x - qa._x * qb._z,
+            qa._w * qb._z + qa._z * qb._w + qa._x * qb._y - qa._y * qb._x,
+            qa._w * qb._w - qa._x * qb._x - qa._y * qb._y - qa._z * qb._z
+        );
     }
 
-    public dot(q: Quaternion): number {
-        return this.x * q.x + this.y * q.y + this.z * q.z + this.w * q.w;
+    public static multiplyScalar(a: Quaternion | number, b: Quaternion | number): Quaternion {
+        if (typeof b === 'number' && a instanceof Quaternion) {
+            return new Quaternion(a._x * b, a._y * b, a._z * b, a._w * b);
+        } if (typeof a === 'number' && b instanceof Quaternion) {
+            return new Quaternion(b._x * a, b._y * a, b._z * a, b._w * a);
+        }
+        throw new Error("Invalid arguments");
+    }
+
+    public static dot(qa: Quaternion, qb: Quaternion): number {
+        return qa._x * qb._x + qa._y * qb._y + qa._z * qb._z + qa._w * qb._w;
     }
 
-    public length(): number {
-        return Math.sqrt(this.dot(this));
+    public static len(qa: Quaternion): number {
+        return Math.sqrt(Quaternion.dot(qa, qa));
     }
 
-    public normalise(): this {
-        const denom = this.length();
-        if (denom < 1.0e-6) {
-            this.copy(Quaternion.identity());
-            return this;
+    public static normalize(qa: Quaternion): Quaternion {
+        const root = Quaternion.len(qa);
+        if (root < 0.00001) {
+            return Quaternion.identity();
         }
-        return this.multiplyScalar(1 / this.length());
+        return Quaternion.multiplyScalar(qa, 1 / root);
     }
 
-    public conjugate(): this {
-        this.x *= -1;
-        this.y *= -1;
-        this.z *= -1;
-        return this;
+    public static inverse(qa: Quaternion): Quaternion {
+        return Quaternion.multiplyScalar(qa, -1);
     }
 }
\ No newline at end of file
diff --git a/src/02-quaternion/Rotator.ts b/src/02-quaternion/Rotator.ts
new file mode 100644
index 0000000..b1dd404
--- /dev/null
+++ b/src/02-quaternion/Rotator.ts
@@ -0,0 +1,116 @@
+import * as THREE from 'three';
+import "dat.gui";
+import { PipelineGUI, PipelineObserver } from "../core/Pipeline";
+import { Quaternion } from "./Quaternion";
+
+const Mode = [ "EDIT", "SLERP" ];
+
+class Rotator implements PipelineObserver, PipelineGUI {
+    private _objectReference: THREE.Object3D;
+    private _mode: string = Mode[0];
+    private _t: number = 0;
+
+    private _orientations: Array<Quaternion>;
+    private _index: number;
+
+    private _guiRoot!: dat.GUI;
+    private _rangeController!: dat.GUIController;
+
+    public constructor(objectReference: THREE.Object3D) {
+        this._objectReference = objectReference;
+        this._orientations = [];
+        this._index = 0;
+
+        // TODO: Extract controls to seperate class
+        document.addEventListener('keydown', (event: KeyboardEvent) => {
+            let step = 0.1;
+
+            if (event.shiftKey) {
+                step = -step;
+            }
+            
+            if (event.key.toLowerCase() === 'a') {
+                this._orientations[this._index].x += step; 
+                this._orientations[this._index].x = this._orientations[this._index].x % (2 * Math.PI);
+            }
+            if (event.key.toLowerCase() === 's') {
+                this._orientations[this._index].y += step; 
+                this._orientations[this._index].y = this._orientations[this._index].y % (2 * Math.PI);
+            }
+            if (event.key.toLowerCase() === 'd') {
+                this._orientations[this._index].z += step; 
+                this._orientations[this._index].z = this._orientations[this._index].z % (2 * Math.PI);
+            }
+        });
+    }
+
+    private addQuaternion(): void {
+        if (this._orientations.length >= 5) {
+            return;
+        }
+        this._orientations.push(new Quaternion());
+        this.createQuaternionGUI(this._guiRoot, this._orientations[this._orientations.length - 1]);
+        if (this._rangeController) {
+            this._rangeController.max(this._orientations.length - 1);
+        }
+    }
+
+    private popQuaternion(): void {
+        if (this._orientations.length <= 0) {
+            return;
+        }
+        this._guiRoot.removeFolder(this._guiRoot.__folders["Quaternion " + this._orientations.length]);
+        this._orientations.pop();
+        if (this._rangeController) {
+            this._rangeController.max(this._orientations.length - 1);
+        }
+    }
+
+    private normaliseQuaternions(): void {
+        for (let i = 0; i < this._orientations.length; i++) {
+            this._orientations[i].copy(Quaternion.normalize(this._orientations[i]));
+        }
+    }
+
+    private createQuaternionGUI(gui: dat.GUI, q: Quaternion): void {
+        const folder = gui.addFolder('Quaternion ' + this._orientations.length);
+        folder.add(q, 'x', -1, 1, .01).listen();
+        folder.add(q, 'y', -1, 1, .01).listen();
+        folder.add(q, 'z', -1, 1, .01).listen();
+        folder.add(q, 'w', -1, 1, .01).listen();
+        folder.closed = false;       
+    }
+
+    gui(gui: dat.GUI): void {
+        const folder = gui.addFolder('Rotator');
+        folder.add(this, "_mode", Mode).listen().name("Mode").onFinishChange(() => {
+            for(let i = 0; i < this._orientations.length; i++) {
+                if (this._mode !== Mode[0])
+                    this._guiRoot.__folders["Quaternion " + (i + 1)].close();
+                else
+                    this._guiRoot.__folders["Quaternion " + (i + 1)].open();
+            }
+        });
+        folder.add(this, "_t", 0, 1, 0.01).listen().name("t");
+        folder.add(this, 'addQuaternion').name("Add Quaternion");
+        folder.add(this, 'popQuaternion').name("Pop Quaternion");
+        folder.add(this, 'normaliseQuaternions').name("Normalise Quaternions");
+        this._rangeController = folder.add(this, '_index', 0, this._orientations.length - 1, 1).name("Index").listen();
+        folder.closed = false;
+        this._guiRoot = folder;
+    }
+
+    update(_deltaTime: number): void {
+        if (this._mode === Mode[0] && this._orientations.length !== 0) {
+            this._objectReference.setRotationFromMatrix(this._orientations[this._index].eulerMatrix);
+        } else if (this._mode === Mode[1] && this._orientations.length > 1) {
+            let result: Quaternion = Quaternion.slerp(this._orientations[0], this._orientations[1], this._t);
+            for (let i = 2; i < this._orientations.length ; i++) {
+                result = Quaternion.slerp(result, this._orientations[i], this._t);
+            }
+            this._objectReference.setRotationFromMatrix(result.eulerMatrix);
+        }
+    }
+}
+
+export {Rotator}
-- 
GitLab