Skip to content
Snippets Groups Projects
Unverified Commit cfd4eda5 authored by Jamie Temple's avatar Jamie Temple
Browse files

enhancement: Quaternions are now always unit

parent 60b39c85
No related branches found
No related tags found
No related merge requests found
......@@ -56,6 +56,7 @@ function demo2() {
const ui = new UI();
const render = new CG.RenderManager('#canvas', { near: 0.1, far: 1000, fov: 45, height: 1 });
ui.addModifiable(render);
// demo1();
......@@ -63,7 +64,6 @@ demo2();
render.render();
ui.addModifiable(render);
import { Matrix4 } from "three";
// https://pbr-book.org/3ed-2018/Geometry_and_Transformations/Animating_Transformations#Quaternions
/**
* @class Quaternion
* @description Class for quaternion. Inspired by prbt book.
* https://pbr-book.org/3ed-2018/Geometry_and_Transformations/Animating_Transformations#Quaternions
*/
export class Quaternion {
public _i: number;
public _j: number;
public _k: number;
public _re: number;
public _x: number;
public _y: number;
public _z: number;
public _w: number;
private _mode: string = "normal";
private _keepUnitQuaternion: boolean;
constructor(re: number = 1, i: number = 0, j: number = 0, k: number = 0) {
this._re = re;
this._i = i;
this._j = j;
this._k = k;
constructor(x: number = 0, y: number = 0, z: number = 0, w: number = 1, keepUnitQuaternion: boolean = true) {
this._x = x;
this._y = y;
this._z = z;
this._w = w;
this._keepUnitQuaternion = keepUnitQuaternion;
}
public get re(): number { return this._re; }
public get i(): number { return this._i; }
public get j(): number { return this._j; }
public get k(): number { return this._k; }
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; }
public get mode(): string { return this._mode; }
public set x(x: number) { this.set(x, this.y, this.z, this.w) }
public set y(y: number) { this.set(this.x, y, this.z, this.w) }
public set z(z: number) { this.set(this.x, this.y, z, this.w) }
public set w(w: number) { this.set(this.x, this.y, this.z, w) }
public set mode(mode: string) { this._mode = mode; }
public static setX(q: Quaternion, x: number): void { q.x = x; }
public static setY(q: Quaternion, y: number): void { q.y = y; }
public static setZ(q: Quaternion, z: number): void { q.z = z; }
public static setW(q: Quaternion, w: number): void { q.w = w; }
public set(x: number, y: number, z: number, w: number): Quaternion {
if (this._keepUnitQuaternion) {
const length = Math.sqrt(x * x + y * y + z * z + w * w);
public set re(re: number) { this._re = re; }
public set i(i: number) { this._i = i; }
public set j(j: number) { this._j = j; }
public set k(k: number) { this._k = k; }
if (length === 0) {
this._x = 0;
this._y = 0;
this._z = 0;
this._w = 1;
} else {
this._x = x / length;
this._y = y / length;
this._z = z / length;
this._w = w / length;
}
return this
}
this._x = x;
this._y = y;
this._z = z;
this._w = w;
return this;
}
public clone(): Quaternion {
return new Quaternion(this.x, this.y, this.z, this.w);
}
public copy(q: Quaternion): Quaternion {
this._re = q.re;
this._i = q.i;
this._j = q.j;
this._k = q.k;
this._x = q.w;
this._y = q.x;
this._z = q.y;
this._w = q.z;
return this;
}
public getMatrix(): Matrix4 {
public toMatrix(): Matrix4 {
let copy = new Quaternion(this.re, this.i, this.j, this.k);
let copy = this.clone();
copy = Quaternion.normalize(copy);
const matrix = new Matrix4();
const x = copy.i;
const y = copy.j;
const z = copy.k;
const w = copy.re;
const x = copy.x;
const y = copy.y;
const z = copy.z;
const w = copy.w;
const m00 = 1 - 2 * y * y - 2 * z * z;
const m01 = 2 * x * y - 2 * w * z;
......@@ -68,76 +115,72 @@ export class Quaternion {
return matrix;
}
public static normalize(q: Quaternion): Quaternion {
const length = Math.sqrt(q.re * q.re + q.i * q.i + q.j * q.j + q.k * q.k);
if (length === 0) {
return new Quaternion(0, 0, 0, 0);
public static dot(q1: Quaternion, q2: Quaternion): number {
return q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z;
}
return new Quaternion(q.re / length, q.i / length, q.j / length, q.k / length);
public static normalize(q: Quaternion): Quaternion {
const length = Math.sqrt(Quaternion.dot(q, q));
if (length === 0) return new Quaternion();
return new Quaternion(q.x / length, q.y / length, q.z / length, q.w / length);
}
public static dot(q1: Quaternion, q2: Quaternion): number {
return q1.re * q2.re + q1.i * q2.i + q1.j * q2.j + q1.k * q2.k;
}
public static multiply(q: Quaternion, other: Quaternion | number): Quaternion {
public static multiplyScalar(q: Quaternion, s: number): Quaternion {
return new Quaternion(q.re * s, q.i * s, q.j * s, q.k * s);
}
if (typeof other === "number")
return new Quaternion(q.x * other, q.y * other, q.z * other, q.w * other);
public static multiply(q1: Quaternion, q2: Quaternion): Quaternion {
return new Quaternion(
q1.re * q2.re - q1.i * q2.i - q1.j * q2.j - q1.k * q2.k,
q1.re * q2.i + q1.i * q2.re + q1.j * q2.k - q1.k * q2.j,
q1.re * q2.j - q1.i * q2.k + q1.j * q2.re + q1.k * q2.i,
q1.re * q2.k + q1.i * q2.j - q1.j * q2.i + q1.k * q2.re
q.w * other.x + q.x * other.w + q.y * other.z - q.z * other.y,
q.w * other.y + q.y * other.w + q.z * other.x - q.x * other.z,
q.w * other.z + q.z * other.w + q.x * other.y - q.y * other.x,
q.w * other.w - q.x * other.x - q.y * other.y - q.z * other.z
);
}
public static add(q1: Quaternion, q2: Quaternion): Quaternion {
return new Quaternion(q1.re + q2.re, q1.i + q2.i, q1.j + q2.j, q1.k + q2.k);
return new Quaternion(q1.x + q2.x, q1.y + q2.y, q1.z + q2.z, q1.w + q2.w);
}
public static subtract(q1: Quaternion, q2: Quaternion): Quaternion {
return new Quaternion(q1.re - q2.re, q1.i - q2.i, q1.j - q2.j, q1.k - q2.k);
return Quaternion.add(q1, Quaternion.multiply(q2, -1));
}
public static inverse(q: Quaternion): Quaternion {
return new Quaternion(q.re, -q.i, -q.j, -q.k);
return new Quaternion(-q.x, -q.y, -q.z, q.w);
}
public static slerp(q1: Quaternion, q2: Quaternion, t: number): Quaternion {
let cq1 = new Quaternion(q1.re, q1.i, q1.j, q1.k);
let cq2 = new Quaternion(q2.re, q2.i, q2.j, q2.k);
let cq1 = q1.clone();
let cq2 = q2.clone();
cq1 = Quaternion.normalize(cq1);
cq2 = Quaternion.normalize(cq2);
let dot = Quaternion.dot(cq1, cq2);
let costheta = Quaternion.dot(cq1, cq2);
if (dot < 0.0) {
if (costheta < 0.0) {
cq2 = Quaternion.inverse(cq2);
dot = -dot;
costheta = -costheta;
}
if (dot > 0.9995) {
return Quaternion.add(cq1, Quaternion.multiplyScalar(
if (costheta > 0.9995) {
return Quaternion.add(cq1, Quaternion.multiply(
Quaternion.subtract(cq2, cq1), t));
}
const theta0 = Math.acos(dot);
const theta = theta0 * t;
const theta = Math.acos(costheta);
const thetat = theta * t;
const sinthetat = Math.sin(thetat);
const sintheta = Math.sin(theta);
const sintheta0 = Math.sin(theta0);
const s0 = Math.cos(theta) - dot * sintheta / sintheta0;
const s1 = sintheta / sintheta0;
const s0 = Math.cos(theta) - costheta * sinthetat / sintheta;
const s1 = sinthetat / sintheta;
return Quaternion.add(
Quaternion.multiplyScalar(cq1, s0),
Quaternion.multiplyScalar(cq2, s1)
Quaternion.multiply(cq1, s0),
Quaternion.multiply(cq2, s1)
)
}
......
import { GUI } from "dat.gui";
import { BufferAttribute, BufferGeometry, Color, Group, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, MeshLambertMaterial, Object3D, SphereBufferGeometry, Vector3 } from "three";
import { BufferGeometry, Color, Group, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, Object3D, SphereBufferGeometry, Vector3 } from "three";
import { Quaternion } from "./Quaternion";
import { Animatable, Updatable, Modifiable } from "./Interfaces";
import { Updatable, Modifiable } from "./Interfaces";
function lerp(c: Color, d: Color, t: number) {
return new Color(
c.r + (d.r - c.r) * t,
c.g + (d.g - c.g) * t,
c.b + (d.b - c.b) * t
);
}
export class RotationObject implements Updatable, Modifiable {
export class RotationObject implements Animatable, Updatable, Modifiable {
public SLERP: boolean = false;
public MODE: string = "Single";
public qa: Quaternion;
public qb: Quaternion;
public t: number;
public doSLERP: boolean;
public useTrigonometry: boolean;
public qa: Quaternion = new Quaternion();;
public qb: Quaternion = new Quaternion();;
public t: number = 0;
private _qworking: Quaternion;
private _mesh?: Mesh;
private _qworking: Quaternion = new Quaternion();;
private _orientation: Group = new Group();
private _coordinate: Group = new Group();
private _rotationAxis: Group = new Group();
private _mesh?: Mesh;
constructor() {
this.qa = new Quaternion(1, 0, 0, 0);
this.qb = new Quaternion(1, 0, 0, 0);
this.doSLERP = false;
this.useTrigonometry = false;
this.t = 0;
this._qworking = new Quaternion(1, 0, 0, 0);
this.createReference();
this.createRotationAxis();
}
objects(): Object3D[] {
return [this._orientation, this._rotationAxis];
}
animate(delta: number): void {
if (!this.doSLERP) return;
this.t = delta;
this._qworking = Quaternion.slerp(this.qa, this.qb, this.t);
this.update();
return [this._coordinate, this._rotationAxis];
}
update(): void {
if (!this.doSLERP)
if (this.SLERP) {
this._qworking = Quaternion.slerp(this.qa, this.qb, this.t);
} else {
if (this.MODE === "Single")
this._qworking = this.qa.clone();
else
this._qworking = Quaternion.multiply(this.qa, this.qb);
}
if (this._mesh) {
this._mesh.setRotationFromMatrix(this._qworking.getMatrix());
this._orientation.setRotationFromMatrix(this._qworking.getMatrix());
this._mesh.setRotationFromMatrix(this._qworking.toMatrix());
this.updateRotationAxis();
}
}
resetQuaternions(): void {
this.qa.set(0, 0, 0, 1);
this.qb.set(0, 0, 0, 1);
this.update();
}
clampQuaternions(): void {
const E = 1e-2;
if (Math.abs(this.qa.x) < E) this.qa.x = 0;
if (Math.abs(this.qa.y) < E) this.qa.y = 0;
if (Math.abs(this.qa.z) < E) this.qa.z = 0;
if (Math.abs(this.qa.w) < E) this.qa.w = 0;
if (Math.abs(this.qb.x) < E) this.qb.x = 0;
if (Math.abs(this.qb.y) < E) this.qb.y = 0;
if (Math.abs(this.qb.z) < E) this.qb.z = 0;
if (Math.abs(this.qb.w) < E) this.qb.w = 0;
}
createElement(gui: GUI): void {
const rot = gui.addFolder("RotationObject");
rot.open();
rot.add(this, "doSLERP");
// rot.add(this, "useTrigonometry");
rot.add(this, "t", 0, 1).onChange(() => { this.animate(this.t) });
const fqa = rot.addFolder("qa");
const fqb = rot.addFolder("qb");
let folder = gui.__folders["Rotation Object"];
if (folder) gui.removeFolder(folder);
folder = gui.addFolder("Rotation Object");
folder.open();
folder.add(this, "SLERP").onFinishChange(() => {
this.createElement(gui);
});
if (this.SLERP) {
this.MODE = "Multi"
folder.add(this, "t", 0, 1, .01).onChange(() => { this.update() });
}
else {
folder.add(this, "MODE", ["Single", "Multi"]).onFinishChange(() => {
this.createElement(gui);
});
}
folder.add(this, "resetQuaternions").name("RESET").onFinishChange(() => {
this.createElement(gui);
});
folder.add(this, "clampQuaternions").name("CLAMP").onFinishChange(() => {
this.createElement(gui);
});
const range: number = 1;
const step: number = .01;
fqa.add(this.qa, "re", -1, 1, .05).onChange(() => { this.update() });
fqa.add(this.qa, "i", -1, 1, .05).onChange(() => { this.update() });
fqa.add(this.qa, "j", -1, 1, .05).onChange(() => { this.update() });
fqa.add(this.qa, "k", -1, 1, .05).onChange(() => { this.update() });
const func = (): void => { this.update(); }
fqb.add(this.qb, "re", -1, 1, .05).onChange(() => { this.update() });
fqb.add(this.qb, "i", -1, 1, .05).onChange(() => { this.update() });
fqb.add(this.qb, "j", -1, 1, .05).onChange(() => { this.update() });
fqb.add(this.qb, "k", -1, 1, .05).onChange(() => { this.update() });
if (this.MODE === "Single" || this.MODE === "Multi") {
const fqa = folder.addFolder(this.MODE === "Single" ? "Quaternion" : "Quaternion A");
fqa.open();
fqa.add(this.qa, "x", -range, range, step).listen().onChange(func);
fqa.add(this.qa, "y", -range, range, step).listen().onChange(func);
fqa.add(this.qa, "z", -range, range, step).listen().onChange(func);
fqa.add(this.qa, "w", -range, range, step).listen().onChange(func);
}
if (this.MODE === "Multi") {
const fqb = folder.addFolder("Quaternion B");
fqb.open();
fqb.add(this.qb, "x", -range, range, step).listen().onChange(func);
fqb.add(this.qb, "y", -range, range, step).listen().onChange(func);
fqb.add(this.qb, "z", -range, range, step).listen().onChange(func);
fqb.add(this.qb, "w", -range, range, step).listen().onChange(func);
}
}
createReference(): void {
......@@ -89,9 +129,9 @@ export class RotationObject implements Animatable, Updatable, Modifiable {
];
const positions: Vector3[] = [
new Vector3(1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(0, 0, 1)
new Vector3(5, 0, 0),
new Vector3(0, 5, 0),
new Vector3(0, 0, 5)
];
let lines: Line[] = [];
......@@ -99,7 +139,7 @@ export class RotationObject implements Animatable, Updatable, Modifiable {
for (let i = 0; i < 3; i++) {
const line = new Line(
new BufferGeometry().setFromPoints([new Vector3(0, 0, 0), positions[i]]),
new BufferGeometry().setFromPoints([positions[i].clone().multiplyScalar(-1), positions[i]]),
new LineBasicMaterial({ color: colors[i].getHex() })
);
line.name = "line" + i;
......@@ -114,8 +154,8 @@ export class RotationObject implements Animatable, Updatable, Modifiable {
spheres.push(sphere);
}
this._orientation.add(...lines);
this._orientation.add(...spheres);
this._coordinate.add(...lines);
this._coordinate.add(...spheres);
}
updateRotationAxis(): void {
......@@ -126,14 +166,14 @@ export class RotationObject implements Animatable, Updatable, Modifiable {
line.geometry.dispose();
const dir = new Vector3(
this._qworking.i,
this._qworking.j,
this._qworking.k
this._qworking.x,
this._qworking.y,
this._qworking.z
);
const origin = new Vector3(0, 0, 0);
line.geometry = new BufferGeometry().setFromPoints([
origin, dir]);
origin, dir.normalize()]);
point.position.copy(dir);
}
......
// class to contain a threejs mesh and quaternion
// rotates the mesh
// allow to modify parameter t
// set rotation axis
// set rotation angle
import { BufferGeometry, Group, Line, LineBasicMaterial, Mesh, MeshBasicMaterial, MeshLambertMaterial, Object3D, SphereGeometry, Vector3 } from "three";
import { Quaternion } from "./Quaternion";
import { Modifiable, Updatable } from "./Interfaces";
import { GUI } from "dat.gui";
import { OBJLoader } from "three/examples/jsm/loaders/OBJLoader";
export class RotObject implements Modifiable, Updatable {
public mesh?: Mesh;
public angle: number;
public group: Group;
public points: Mesh[] = [];
public axes: Line[] = [];
public linegeom: BufferGeometry[];
public linematerial: LineBasicMaterial[];
public pointgeom: SphereGeometry;
public pointmaterial: MeshBasicMaterial[];
public quaternion: Quaternion;
public t: number;
public orientationA: Quaternion;
public orientationB: Quaternion;
constructor(quaternion: Quaternion, t: number) {
this.createRotationMesh();
this.quaternion = quaternion;
this.orientationA = new Quaternion(1, 0, 0, 0);
this.orientationB = new Quaternion(-1, 0, 0, 0);
this.t = t;
this.angle = 0;
this.pointgeom = new SphereGeometry(0.05, 32, 32);
this.linegeom = [
new BufferGeometry().setFromPoints([new Vector3(-1.1, 0, 0), new Vector3(1.1, 0, 0)]),
new BufferGeometry().setFromPoints([new Vector3(0, -1.1, 0), new Vector3(0, 1.1, 0)]),
new BufferGeometry().setFromPoints([new Vector3(0, 0, -1.1), new Vector3(0, 0, 1.1)])
];
this.pointmaterial = [
new MeshBasicMaterial({ color: 0xff0000 }), // i
new MeshBasicMaterial({ color: 0x00ff00 }), // j
new MeshBasicMaterial({ color: 0x0000ff }) // k
];
this.linematerial = [
new LineBasicMaterial({ color: 0xff0000 }), // x
new LineBasicMaterial({ color: 0x00ff00 }), // y
new LineBasicMaterial({ color: 0x0000ff }) // z
];
// i, j, k
this.points[0] = new Mesh(this.pointgeom, this.pointmaterial[0]);
this.points[0].name = "i";
this.points[0].translateX(1.1);
this.points[1] = new Mesh(this.pointgeom, this.pointmaterial[1]);
this.points[1].name = "j";
this.points[1].translateY(1.1);
this.points[2] = new Mesh(this.pointgeom, this.pointmaterial[2]);
this.points[2].name = "k";
this.points[2].translateZ(1.1);
// x, y, z
this.axes[0] = new Line(this.linegeom[0], this.linematerial[0]);
this.axes[0].name = "x-axis";
this.axes[1] = new Line(this.linegeom[1], this.linematerial[1]);
this.axes[1].name = "y-axis";
this.axes[2] = new Line(this.linegeom[2], this.linematerial[2]);
this.axes[2].name = "z-axis";
this.group = new Group();
this.group.add(...this.points, ...this.axes);
}
createRotationMesh(): void {
let ref_mesh = this.mesh;
new OBJLoader().load("../Suzanne.obj",
function (obj) {
ref_mesh = obj.children[0] as Mesh;
ref_mesh.material = new MeshLambertMaterial({ wireframe: true, side: 1 });
}
)
}
setMesh(mesh: Mesh): void {
this.mesh = mesh;
}
createElement(gui: GUI): void {
const folder = gui.addFolder("RotObject");
folder.add(this, "t", 0, 1, 0.01).name("t").onChange(() => {
Quaternion.normalize(this.orientationA);
Quaternion.normalize(this.orientationB);
const rot = Quaternion.slerp(this.orientationA, this.orientationB, this.t);
this.quaternion.copy(rot);
Quaternion.normalize(this.quaternion);
this.update();
gui.updateDisplay();
gui.__controllers.forEach((controller) => {
controller.updateDisplay();
})
});
folder.add(this, "angle", 1, 360, 1).name("angle (deg)").onChange(() => {
const half_angle = 0.5 * this.angle;
this.quaternion.re = Math.cos(half_angle * Math.PI / 180);
const sum = this.quaternion.i + this.quaternion.j + this.quaternion.k;
if (sum > 0) {
this.quaternion.i = this.quaternion.i / sum;
this.quaternion.j = this.quaternion.j / sum;
this.quaternion.k = this.quaternion.k / sum;
}
else {
this.quaternion.i = 0;
this.quaternion.j = 0;
this.quaternion.k = 0;
}
this.quaternion.i = this.quaternion.i * Math.sin(half_angle * Math.PI / 180);
this.quaternion.j = this.quaternion.j * Math.sin(half_angle * Math.PI / 180);
this.quaternion.k = this.quaternion.k * Math.sin(half_angle * Math.PI / 180);
this.update();
})
folder.add(this.quaternion, "re", -1, 1, 0.01).name("re").onChange(() => {
this.update();
});
folder.add(this.quaternion, "i", -1, 1, 0.01).name("i").onChange(() => {
this.update();
});
folder.add(this.quaternion, "j", -1, 1, 0.01).name("j").onChange(() => {
this.update();
});
folder.add(this.quaternion, "k", -1, 1, 0.01).name("k").onChange(() => {
this.update();
});
const folder2 = gui.addFolder("OrientationA");
folder2.add(this.orientationA, "re", -1, 1, 0.01).name("re").onChange(() => {
this.update();
});
folder2.add(this.orientationA, "i", -1, 1, 0.01).name("i").onChange(() => {
this.update();
});
folder2.add(this.orientationA, "j", -1, 1, 0.01).name("j").onChange(() => {
this.update();
});
folder2.add(this.orientationA, "k", -1, 1, 0.01).name("k").onChange(() => {
this.update();
});
const folder3 = gui.addFolder("OrientationB");
folder3.add(this.orientationB, "re", -1, 1, 0.01).name("re").onChange(() => {
this.update();
});
folder3.add(this.orientationB, "i", -1, 1, 0.01).name("i").onChange(() => {
this.update();
});
folder3.add(this.orientationB, "j", -1, 1, 0.01).name("j").onChange(() => {
this.update();
});
folder3.add(this.orientationB, "k", -1, 1, 0.01).name("k").onChange(() => {
this.update();
});
folder.open();
}
objects(): Object3D[] {
return [this.group];
}
update(): void {
if (this.mesh) {
this.mesh.setRotationFromMatrix(this.quaternion.getMatrix());
}
if (this.group) {
this.group.setRotationFromMatrix(this.quaternion.getMatrix());
}
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment