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