Skip to content

Instantly share code, notes, and snippets.

@alexsr
Last active April 3, 2023 18:47
Show Gist options
  • Save alexsr/5065f0189a7af13b2f3bc43d22aff62f to your computer and use it in GitHub Desktop.
Save alexsr/5065f0189a7af13b2f3bc43d22aff62f to your computer and use it in GitHub Desktop.
SVD and other matrix decompositions on 3x3 matrices written in GLSL
// This is a GLSL implementation of
// "Computing the Singular Value Decomposition of 3 x 3 matrices with
// minimal branching and elementary floating point operations"
// by Aleka McAdams et.al.
// http://pages.cs.wisc.edu/~sifakis/papers/SVD_TR1690.pdf
// This should also work on the CPU using glm
// Then you probably should use glm::quat instead of vec4
// and mat3_cast to convert to mat3.
// GAMMA = 3 + sqrt(8)
// C_STAR = cos(pi/8)
// S_STAR = sin(pi/8)
#define GAMMA 5.8284271247
#define C_STAR 0.9238795325
#define S_STAR 0.3826834323
#define SVD_EPS 0.0000001
vec2 approx_givens_quat(float s_pp, float s_pq, float s_qq) {
float c_h = 2 * (s_pp - s_qq);
float s_h2 = s_pq * s_pq;
float c_h2 = c_h * c_h;
if (GAMMA * s_h2 < c_h2) {
float omega = 1.0f / sqrt(s_h2 + c_h2);
return vec2(omega * c_h, omega * s_pq);
}
return vec2(C_STAR, S_STAR);
}
// the quaternion is stored in vec4 like so:
// (c, s * vec3) meaning that .x = c
mat3 quat_to_mat3(vec4 quat) {
float qx2 = quat.y * quat.y;
float qy2 = quat.z * quat.z;
float qz2 = quat.w * quat.w;
float qwqx = quat.x * quat.y;
float qwqy = quat.x * quat.z;
float qwqz = quat.x * quat.w;
float qxqy = quat.y * quat.z;
float qxqz = quat.y * quat.w;
float qyqz = quat.z * quat.w;
return mat3(1.0f - 2.0f * (qy2 + qz2), 2.0f * (qxqy + qwqz), 2.0f * (qxqz - qwqy),
2.0f * (qxqy - qwqz), 1.0f - 2.0f * (qx2 + qz2), 2.0f * (qyqz + qwqx),
2.0f * (qxqz + qwqy), 2.0f * (qyqz - qwqx), 1.0f - 2.0f * (qx2 + qy2));
}
mat3 symmetric_eigenanalysis(mat3 A) {
mat3 S = transpose(A) * A;
// jacobi iteration
mat3 q = mat3(1.0f);
for (int i = 0; i < 5; i++) {
vec2 ch_sh = approx_givens_quat(S[0].x, S[0].y, S[1].y);
vec4 ch_sh_quat = vec4(ch_sh.x, 0, 0, ch_sh.y);
mat3 q_mat = quat_to_mat3(ch_sh_quat);
S = transpose(q_mat) * S * q_mat;
q = q * q_mat;
ch_sh = approx_givens_quat(S[0].x, S[0].z, S[2].z);
ch_sh_quat = vec4(ch_sh.x, 0, -ch_sh.y, 0);
q_mat = quat_to_mat3(ch_sh_quat);
S = transpose(q_mat) * S * q_mat;
q = q * q_mat;
ch_sh = approx_givens_quat(S[1].y, S[1].z, S[2].z);
ch_sh_quat = vec4(ch_sh.x, ch_sh.y, 0, 0);
q_mat = quat_to_mat3(ch_sh_quat);
S = transpose(q_mat) * S * q_mat;
q = q * q_mat;
}
return q;
}
vec2 approx_qr_givens_quat(float a0, float a1) {
float rho = sqrt(a0 * a0 + a1 * a1);
float s_h = a1;
float max_rho_eps = rho;
if (rho <= SVD_EPS) {
s_h = 0;
max_rho_eps = SVD_EPS;
}
float c_h = max_rho_eps + a0;
if (a0 < 0) {
float temp = c_h - 2 * a0;
c_h = s_h;
s_h = temp;
}
float omega = 1.0f / sqrt(c_h * c_h + s_h * s_h);
return vec2(omega * c_h, omega * s_h);
}
struct QR_mats {
mat3 Q;
mat3 R;
};
QR_mats qr_decomp(mat3 B) {
QR_mats qr_decomp_result;
mat3 R;
// 1 0
// (ch, 0, 0, sh)
vec2 ch_sh10 = approx_qr_givens_quat(B[0].x, B[0].y);
mat3 Q10 = quat_to_mat3(vec4(ch_sh10.x, 0, 0, ch_sh10.y));
R = transpose(Q10) * B;
// 2 0
// (ch, 0, -sh, 0)
vec2 ch_sh20 = approx_qr_givens_quat(R[0].x, R[0].z);
mat3 Q20 = quat_to_mat3(vec4(ch_sh20.x, 0, -ch_sh20.y, 0));
R = transpose(Q20) * R;
// 2 1
// (ch, sh, 0, 0)
vec2 ch_sh21 = approx_qr_givens_quat(R[1].y, R[1].z);
mat3 Q21 = quat_to_mat3(vec4(ch_sh21.x, ch_sh21.y, 0, 0));
R = transpose(Q21) * R;
qr_decomp_result.R = R;
qr_decomp_result.Q = Q10 * Q20 * Q21;
return qr_decomp_result;
}
struct SVD_mats {
mat3 U;
mat3 Sigma;
mat3 V;
};
SVD_mats svd(mat3 A) {
SVD_mats svd_result;
svd_result.V = symmetric_eigenanalysis(A);
mat3 B = A * svd_result.V;
// sort singular values
float rho0 = dot(B[0], B[0]);
float rho1 = dot(B[1], B[1]);
float rho2 = dot(B[2], B[2]);
if (rho0 < rho1) {
vec3 temp = B[1];
B[1] = -B[0];
B[0] = temp;
temp = svd_result.V[1];
svd_result.V[1] = -svd_result.V[0];
svd_result.V[0] = temp;
float temp_rho = rho0;
rho0 = rho1;
rho1 = temp_rho;
}
if (rho0 < rho2) {
vec3 temp = B[2];
B[2] = -B[0];
B[0] = temp;
temp = svd_result.V[2];
svd_result.V[2] = -svd_result.V[0];
svd_result.V[0] = temp;
rho2 = rho0;
}
if (rho1 < rho2) {
vec3 temp = B[2];
B[2] = -B[1];
B[1] = temp;
temp = svd_result.V[2];
svd_result.V[2] = -svd_result.V[1];
svd_result.V[1] = temp;
}
QR_mats QR = qr_decomp(B);
svd_result.U = QR.Q;
svd_result.Sigma = QR.R;
return svd_result;
}
struct UP_mats {
mat3 U;
mat3 P;
};
UP_mats SVD_to_polar(SVD_mats B) {
UP_mats polar;
polar.P = B.V * B.Sigma * transpose(B.V);
polar.U = B.U * transpose(B.V);
return polar;
}
UP_mats polar_decomp(mat3 A) {
SVD_mats B = svd(A);
UP_mats polar;
polar.P = B.V * B.Sigma * transpose(B.V);
polar.U = B.U * transpose(B.V);
return polar;
}
@alexsr
Copy link
Author

alexsr commented May 20, 2021

The SVD decomposes a matrix into 3 matrices where SVD(A) = U Sigma V_transposed
The diagonal entries of Sigma are the so called singular values.
https://en.wikipedia.org/wiki/Singular_value_decomposition

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment