/* * JaamSim Discrete Event Simulation * Copyright (C) 2012 Ausenco Engineering Canada Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.jaamsim.math; /** * Quaternion class, stored as an array of 4 doubles * @author Matt Chudleigh * */ public class Quaternion { public double x; public double y; public double z; public double w; /** * Default returns an identity quaternion (real = 1, imaginary = 0) */ public Quaternion() { x = 0.0d; y = 0.0d; z = 0.0d; w = 1.0d; } /** * Quaternion constructor with explicit data, may need to be explicitly normalized * @param ix * @param iy * @param iz * @param r */ public Quaternion(double ix, double iy, double iz, double r) { x = ix; y = iy; z = iz; w = r; } public Quaternion(Quaternion q) { x = q.x; y = q.y; z = q.z; w = q.w; } /** * Set this Quaternion with the values (q.x, q.y, q.z, q.w); * @param q the Quaternion containing the values * @throws NullPointerException if q is null */ public void set(Quaternion q) { this.x = q.x; this.y = q.y; this.z = q.z; this.w = q.w; } /** * Set this Quaternion from Euler angles, specifically the kind of euler angles * used by Java3d (which seems to be rotation around global x, then y, then z) * @param v the Vec3d containing the x,y,z angles * @throws NullPointerException if v is null */ public void setEuler3(Vec3d v) { // This will almost certainly be a performance bottleneck before too long Quaternion tmp = new Quaternion(); this.setRotXAxis(v.x); tmp.setRotYAxis(v.y); this.mult(tmp, this); tmp.setRotZAxis(v.z); this.mult(tmp, this); } /** * Set this Quaternion to a rotation about the X-axis. * @param angle the angle to rotate through */ public void setRotXAxis(double angle) { double halfAngle = 0.5d * angle; this.x = Math.sin(halfAngle); this.y = 0.0d; this.z = 0.0d; this.w = Math.cos(halfAngle); } /** * Set this Quaternion to a rotation about the Y-axis. * @param angle the angle to rotate through */ public void setRotYAxis(double angle) { double halfAngle = 0.5d * angle; this.x = 0.0d; this.y = Math.sin(halfAngle); this.z = 0.0d; this.w = Math.cos(halfAngle); } /** * Set this Quaternion to a rotation about the Z-axis. * @param angle the angle to rotate through */ public void setRotZAxis(double angle) { double halfAngle = 0.5d * angle; this.x = 0.0d; this.y = 0.0d; this.z = Math.sin(halfAngle); this.w = Math.cos(halfAngle); } /** * Set this Quaternion from a rotation in axis-angle form. * @param axis the about which to rotate * @param angle the angle to rotate through in radians * @throws NullPointerException if axis is null */ public void setAxisAngle(Vec3d axis, double angle) { double halfAngle = 0.5d * angle; Vec3d v = new Vec3d(axis); v.normalize3(); v.scale3(Math.sin(halfAngle)); this.x = v.x; this.y = v.y; this.z = v.z; this.w = Math.cos(halfAngle); } /** * Factory that returns a Quaternion that would rotate one direction into the other. * Only valid for vectors of the same length * @param from * @param to * @return */ public static Quaternion transformVectors(Vec4d from, Vec4d to) { Vec4d f = new Vec4d(from); Vec4d t = new Vec4d(to); f.normalize3(); t.normalize3(); Vec4d cross = new Vec4d(0.0d, 0.0d, 0.0d, 1.0d); cross.cross3(f, t); double angle = Math.asin(cross.mag3()); cross.normalize3(); Quaternion ret = new Quaternion(); ret.setAxisAngle(cross, angle); return ret; } private final double _dot4(Quaternion q1, Quaternion q2) { double ret; ret = q1.x * q2.x; ret += q1.y * q2.y; ret += q1.z * q2.z; ret += q1.w * q2.w; return ret; } public double magSquared() { return _dot4(this, this); } public double mag() { return Math.sqrt(_dot4(this, this)); } private void _norm(Quaternion q) { double mag = _dot4(q, q); if (MathUtils.isSmall(mag)) { // The quaternion is of length 0, simply return an identity this.x = 0.0d; this.y = 0.0d; this.z = 0.0d; this.w = 1.0d; return; } mag = Math.sqrt(mag); this.x = q.x / mag; this.y = q.y / mag; this.z = q.z / mag; this.w = q.w / mag; } /** * Normalize the quaternion in place */ public void normalize() { _norm(this); } /** * Set this quaternion to the normalized value of q * @throws NullPointerException if q is null */ public void normalize(Quaternion q) { _norm(q); } /** * Set this Quarternion to its complex conjugate */ public void conjugate() { x *= -1.0d; y *= -1.0d; z *= -1.0d; } /** * Set this Quarternion to the complex conjugate of q * @throws NullPointerException if q is null */ public void conjugate(Quaternion q) { this.x = q.x * -1.0d; this.y = q.y * -1.0d; this.z = q.z * -1.0d; this.w = q.w; } /** * Quaternion multiplication, mathematically equivalent to applying both rotations in order. * Sets this to a*b * @param q * @param res */ public void mult(Quaternion a, Quaternion b) { double _x = a.w*b.x + a.x*b.w + a.y*b.z - a.z*b.y; double _y = a.w*b.y + a.y*b.w + a.z*b.x - a.x*b.z; double _z = a.w*b.z + a.z*b.w + a.x*b.y - a.y*b.x; double _w = a.w*b.w - a.x*b.x - a.y*b.y - a.z*b.z; x = _x; y = _y; z = _z; w = _w; } public boolean isNormal() { double magSquared = magSquared(); return MathUtils.near(magSquared, 1.0); } public double dot(Quaternion q) { return _dot4(this, q); } /** * Weighted linear interpolation between quaternions when * weight = 1 -> res = q * weight = 0 -> res = this * @param q - the other quaternion * @param weight - the weight to blend with * @param res - the result */ public void lerp(Quaternion q, double weight, Quaternion res) { double weight1 = 1.0d - weight; res.x = x * weight1 + q.x * weight; res.y = y * weight1 + q.y * weight; res.z = z * weight1 + q.z * weight; res.w = w * weight1 + q.w * weight; } /** * Spherical linear interpolation between quaternions, look up slerp if you are unsure * weight = 1 -> res = q * weight = 0 -> res = this * @param q - the other quaternion * @param weight - the weight to blend with * @param res - the result */ public void slerp(Quaternion q, double weight, Quaternion res) { double cosTheta = dot(q); if (cosTheta > 0.95) { // close enough, just lerp it lerp(q, weight, res); res.normalize(); return; } double theta = Math.acos(cosTheta); double sinTheta = Math.sin(theta); if (MathUtils.isSmall(sinTheta)) { // TODO: some kind of decent default as the two quaternions are nearly opposite throw new IllegalArgumentException("Cannot slerp two opposite quaternions"); } double thisScale = Math.sin((1.0 - weight)*theta) / sinTheta; double qScale = Math.sin(weight*theta) / sinTheta; res.x = x * thisScale + q.x * qScale; res.y = y * thisScale + q.y * qScale; res.z = z * thisScale + q.z * qScale; res.w = w * thisScale + q.w * qScale; } @Override public boolean equals(Object o) { if (!(o instanceof Quaternion)) return false; Quaternion q = (Quaternion)o; return q.x == x && q.y == y && q.z == z && q.w == w; } public boolean near(Quaternion q) { return MathUtils.near(x, q.x) && MathUtils.near(y, q.y) && MathUtils.near(z, q.z) && MathUtils.near(w, q.w); } @Override public int hashCode() { assert false : "hashCode not designed"; return 42; // any arbitrary constant will do } @Override public String toString() { return "[(" + x + ", " + y + ", " + z + ")i, " + w + "]"; } } // class Quaternion