diff --git a/src/main.ts b/src/main.ts index f5743bae6a56dee254ccbe21487ac19de29a4cbe..66e18897c45b630650160ab00ca8b76f6edf92bf 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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); diff --git a/src/uitls/Quaternion.ts b/src/uitls/Quaternion.ts index 5ad000003ae286b65dc98477e3d40916ab41f103..60fc67b80ac68f6027a807ced7ac559ebc924ca6 100644 --- a/src/uitls/Quaternion.ts +++ b/src/uitls/Quaternion.ts @@ -1,50 +1,97 @@ 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); + + 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 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; } + 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); - } - - return new Quaternion(q.re / length, q.i / length, q.j / length, q.k / 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; + return q1.w * q2.w + q1.x * q2.x + q1.y * q2.y + q1.z * q2.z; } - public static multiplyScalar(q: Quaternion, s: number): Quaternion { - return new Quaternion(q.re * s, q.i * s, q.j * s, q.k * s); + 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 multiply(q1: Quaternion, q2: Quaternion): Quaternion { + public static multiply(q: Quaternion, other: Quaternion | number): Quaternion { + + if (typeof other === "number") + return new Quaternion(q.x * other, q.y * other, q.z * other, q.w * other); + 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) ) } diff --git a/src/uitls/RotationObject.ts b/src/uitls/RotationObject.ts index 8e53d8dc8a71859f8ee20be8e51479098d872312..beca6bb6f1298ada31570d47234bc2e2cc25e48b 100644 --- a/src/uitls/RotationObject.ts +++ b/src/uitls/RotationObject.ts @@ -1,84 +1,124 @@ 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) - this._qworking = Quaternion.multiply(this.qa, this.qb); + 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"); - - 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() }); - - 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() }); + 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; + + const func = (): void => { 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); } diff --git a/src/uitls/bak.RotObject.ts b/src/uitls/bak.RotObject.ts deleted file mode 100644 index 750683d6d73a5a8ac4ed1829ead6f8ee8c98085f..0000000000000000000000000000000000000000 --- a/src/uitls/bak.RotObject.ts +++ /dev/null @@ -1,197 +0,0 @@ -// 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