/* * * Singe 3 * Copyright (C) 2006-2026 Scott Duensing * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. * */ // Vector, quaternion and matrix arithmetic for the 3D scene. Small on purpose: only what a scene // graph, a camera and glTF animation need. #include #include #include "math3d.h" // Translation * rotation * scale, the glTF node transform. Mat4T mat4Compose(Vec3T translation, QuatT rotation, Vec3T scale) { Mat4T out; float xx = rotation.x * rotation.x; float yy = rotation.y * rotation.y; float zz = rotation.z * rotation.z; float xy = rotation.x * rotation.y; float xz = rotation.x * rotation.z; float yz = rotation.y * rotation.z; float wx = rotation.w * rotation.x; float wy = rotation.w * rotation.y; float wz = rotation.w * rotation.z; out.m[0] = (1.0f - 2.0f * (yy + zz)) * scale.x; out.m[1] = (2.0f * (xy + wz)) * scale.x; out.m[2] = (2.0f * (xz - wy)) * scale.x; out.m[3] = 0.0f; out.m[4] = (2.0f * (xy - wz)) * scale.y; out.m[5] = (1.0f - 2.0f * (xx + zz)) * scale.y; out.m[6] = (2.0f * (yz + wx)) * scale.y; out.m[7] = 0.0f; out.m[8] = (2.0f * (xz + wy)) * scale.z; out.m[9] = (2.0f * (yz - wx)) * scale.z; out.m[10] = (1.0f - 2.0f * (xx + yy)) * scale.z; out.m[11] = 0.0f; out.m[12] = translation.x; out.m[13] = translation.y; out.m[14] = translation.z; out.m[15] = 1.0f; return out; } // Splits a TRS matrix back into its parts (glTF nodes may carry a matrix instead of TRS). void mat4Decompose(Mat4T a, Vec3T *translation, QuatT *rotation, Vec3T *scale) { Vec3T x = vec3(a.m[0], a.m[1], a.m[2]); Vec3T y = vec3(a.m[4], a.m[5], a.m[6]); Vec3T z = vec3(a.m[8], a.m[9], a.m[10]); Mat4T r = mat4Identity(); *translation = vec3(a.m[12], a.m[13], a.m[14]); *scale = vec3(vec3Length(x), vec3Length(y), vec3Length(z)); // A negative determinant means one axis is mirrored; put the flip on X. if (vec3Dot(vec3Cross(x, y), z) < 0.0f) { scale->x = -scale->x; } x = vec3Scale(x, (scale->x != 0.0f) ? 1.0f / scale->x : 0.0f); y = vec3Scale(y, (scale->y != 0.0f) ? 1.0f / scale->y : 0.0f); z = vec3Scale(z, (scale->z != 0.0f) ? 1.0f / scale->z : 0.0f); r.m[0] = x.x; r.m[1] = x.y; r.m[2] = x.z; r.m[4] = y.x; r.m[5] = y.y; r.m[6] = y.z; r.m[8] = z.x; r.m[9] = z.y; r.m[10] = z.z; *rotation = quatFromMat4(r); } Mat4T mat4Identity(void) { Mat4T out; memset(&out, 0, sizeof(out)); out.m[0] = 1.0f; out.m[5] = 1.0f; out.m[10] = 1.0f; out.m[15] = 1.0f; return out; } // General 4x4 inverse by cofactors. Returns false for a singular matrix (out untouched): one // whose determinant is zero or too small to take a finite reciprocal of, so a node scaled down by // any ordinary amount still inverts. bool mat4Invert(Mat4T a, Mat4T *out) { float inv[16]; float det; int32_t x; const float *m = a.m; inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15] + m[9] * m[7] * m[14] + m[13] * m[6] * m[11] - m[13] * m[7] * m[10]; inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15] - m[8] * m[7] * m[14] - m[12] * m[6] * m[11] + m[12] * m[7] * m[10]; inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15] + m[8] * m[7] * m[13] + m[12] * m[5] * m[11] - m[12] * m[7] * m[9]; inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14] - m[8] * m[6] * m[13] - m[12] * m[5] * m[10] + m[12] * m[6] * m[9]; inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15] - m[9] * m[3] * m[14] - m[13] * m[2] * m[11] + m[13] * m[3] * m[10]; inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15] + m[8] * m[3] * m[14] + m[12] * m[2] * m[11] - m[12] * m[3] * m[10]; inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15] - m[8] * m[3] * m[13] - m[12] * m[1] * m[11] + m[12] * m[3] * m[9]; inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14] + m[8] * m[2] * m[13] + m[12] * m[1] * m[10] - m[12] * m[2] * m[9]; inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15] + m[5] * m[3] * m[14] + m[13] * m[2] * m[7] - m[13] * m[3] * m[6]; inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15] - m[4] * m[3] * m[14] - m[12] * m[2] * m[7] + m[12] * m[3] * m[6]; inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15] + m[4] * m[3] * m[13] + m[12] * m[1] * m[7] - m[12] * m[3] * m[5]; inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14] - m[4] * m[2] * m[13] - m[12] * m[1] * m[6] + m[12] * m[2] * m[5]; inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11] - m[5] * m[3] * m[10] - m[9] * m[2] * m[7] + m[9] * m[3] * m[6]; inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11] + m[4] * m[3] * m[10] + m[8] * m[2] * m[7] - m[8] * m[3] * m[6]; inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11] - m[4] * m[3] * m[9] - m[8] * m[1] * m[7] + m[8] * m[3] * m[5]; inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10] + m[4] * m[2] * m[9] + m[8] * m[1] * m[6] - m[8] * m[2] * m[5]; det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12]; if ((det == 0.0f) || !isfinite(1.0f / det)) { return false; } det = 1.0f / det; for (x = 0; x < 16; x++) { out->m[x] = inv[x] * det; } return true; } // A view matrix: the camera at eye looking at target. Mat4T mat4LookAt(Vec3T eye, Vec3T target, Vec3T up) { Mat4T out; Vec3T f = vec3Normalize(vec3Subtract(target, eye)); Vec3T s = vec3Normalize(vec3Cross(f, up)); Vec3T u = vec3Cross(s, f); out.m[0] = s.x; out.m[1] = u.x; out.m[2] = -f.x; out.m[3] = 0.0f; out.m[4] = s.y; out.m[5] = u.y; out.m[6] = -f.y; out.m[7] = 0.0f; out.m[8] = s.z; out.m[9] = u.z; out.m[10] = -f.z; out.m[11] = 0.0f; out.m[12] = -vec3Dot(s, eye); out.m[13] = -vec3Dot(u, eye); out.m[14] = vec3Dot(f, eye); out.m[15] = 1.0f; return out; } // a * b: applies b first, then a. Mat4T mat4Multiply(Mat4T a, Mat4T b) { Mat4T out; int32_t column; int32_t row; int32_t k; float sum; for (column = 0; column < 4; column++) { for (row = 0; row < 4; row++) { sum = 0.0f; for (k = 0; k < 4; k++) { sum += a.m[k * 4 + row] * b.m[column * 4 + k]; } out.m[column * 4 + row] = sum; } } return out; } // The matrix that carries normals under a's rotation and scale: the inverse transpose of its // upper 3x3, taken as the cofactor matrix over the determinant (no 4x4 inverse needed). Under a // singular matrix (a zero scale) the normals keep their direction. Mat4T mat4NormalMatrix(Mat4T a) { Mat4T out = mat4Identity(); const float *m = a.m; float c[9]; float det; int32_t x; c[0] = m[5] * m[10] - m[6] * m[9]; c[1] = m[6] * m[8] - m[4] * m[10]; c[2] = m[4] * m[9] - m[5] * m[8]; c[3] = m[2] * m[9] - m[1] * m[10]; c[4] = m[0] * m[10] - m[2] * m[8]; c[5] = m[1] * m[8] - m[0] * m[9]; c[6] = m[1] * m[6] - m[2] * m[5]; c[7] = m[2] * m[4] - m[0] * m[6]; c[8] = m[0] * m[5] - m[1] * m[4]; det = m[0] * c[0] + m[1] * c[1] + m[2] * c[2]; if ((det == 0.0f) || !isfinite(1.0f / det)) { return out; } det = 1.0f / det; for (x = 0; x < 9; x++) { out.m[(x / 3) * 4 + (x % 3)] = c[x] * det; } return out; } // Depth maps to 0..1 (what SDL_GPU expects on every backend). Mat4T mat4Orthographic(float width, float height, float near, float far) { Mat4T out; memset(&out, 0, sizeof(out)); out.m[0] = 2.0f / width; out.m[5] = 2.0f / height; out.m[10] = -1.0f / (far - near); out.m[14] = -near / (far - near); out.m[15] = 1.0f; return out; } // An off-centre parallel projection (a shadow map fitted to what it covers). Mat4T mat4OrthographicBounds(float left, float right, float bottom, float top, float near, float far) { Mat4T out; memset(&out, 0, sizeof(out)); out.m[0] = 2.0f / (right - left); out.m[5] = 2.0f / (top - bottom); out.m[10] = -1.0f / (far - near); out.m[12] = -(right + left) / (right - left); out.m[13] = -(top + bottom) / (top - bottom); out.m[14] = -near / (far - near); out.m[15] = 1.0f; return out; } Mat4T mat4Perspective(float fovDegrees, float aspect, float near, float far) { Mat4T out; float f = 1.0f / tanf(DEGREES_TO_RADIANS(fovDegrees) / 2.0f); memset(&out, 0, sizeof(out)); out.m[0] = f / aspect; out.m[5] = f; out.m[10] = far / (near - far); out.m[11] = -1.0f; out.m[14] = (near * far) / (near - far); return out; } // A point through a matrix, divided by the clip w it comes out with (which is handed back for // callers that need to know which side of the camera the point is on). Vec3T mat4Project(Mat4T a, Vec3T p, float *w) { Vec3T out; *w = a.m[3] * p.x + a.m[7] * p.y + a.m[11] * p.z + a.m[15]; out.x = a.m[0] * p.x + a.m[4] * p.y + a.m[8] * p.z + a.m[12]; out.y = a.m[1] * p.x + a.m[5] * p.y + a.m[9] * p.z + a.m[13]; out.z = a.m[2] * p.x + a.m[6] * p.y + a.m[10] * p.z + a.m[14]; if (fabsf(*w) > MATH_EPSILON) { out.x /= *w; out.y /= *w; out.z /= *w; } return out; } Vec3T mat4TransformPoint(Mat4T a, Vec3T p) { float w; return mat4Project(a, p, &w); } // Directions ignore translation. Vec3T mat4TransformVector(Mat4T a, Vec3T v) { Vec3T out; out.x = a.m[0] * v.x + a.m[4] * v.y + a.m[8] * v.z; out.y = a.m[1] * v.x + a.m[5] * v.y + a.m[9] * v.z; out.z = a.m[2] * v.x + a.m[6] * v.y + a.m[10] * v.z; return out; } Mat4T mat4Transpose(Mat4T a) { Mat4T out; int32_t column; int32_t row; for (column = 0; column < 4; column++) { for (row = 0; row < 4; row++) { out.m[column * 4 + row] = a.m[row * 4 + column]; } } return out; } QuatT quatFromAxisAngle(Vec3T axis, float degrees) { QuatT out; float half = DEGREES_TO_RADIANS(degrees) / 2.0f; float s = sinf(half); Vec3T n = vec3Normalize(axis); out.x = n.x * s; out.y = n.y * s; out.z = n.z * s; out.w = cosf(half); return out; } // Intrinsic rotations applied in the order Y (yaw), X (pitch), Z (roll), matching what a script // means by "turn, then tilt, then bank". QuatT quatFromEuler(float xDegrees, float yDegrees, float zDegrees) { QuatT qx = quatFromAxisAngle(vec3(1.0f, 0.0f, 0.0f), xDegrees); QuatT qy = quatFromAxisAngle(vec3(0.0f, 1.0f, 0.0f), yDegrees); QuatT qz = quatFromAxisAngle(vec3(0.0f, 0.0f, 1.0f), zDegrees); return quatMultiply(quatMultiply(qy, qx), qz); } // The rotation part of a matrix, assuming no scale. QuatT quatFromMat4(Mat4T a) { QuatT out; float trace = a.m[0] + a.m[5] + a.m[10]; float s; if (trace > 0.0f) { s = sqrtf(trace + 1.0f) * 2.0f; out.w = 0.25f * s; out.x = (a.m[6] - a.m[9]) / s; out.y = (a.m[8] - a.m[2]) / s; out.z = (a.m[1] - a.m[4]) / s; } else if ((a.m[0] > a.m[5]) && (a.m[0] > a.m[10])) { s = sqrtf(1.0f + a.m[0] - a.m[5] - a.m[10]) * 2.0f; out.w = (a.m[6] - a.m[9]) / s; out.x = 0.25f * s; out.y = (a.m[4] + a.m[1]) / s; out.z = (a.m[8] + a.m[2]) / s; } else if (a.m[5] > a.m[10]) { s = sqrtf(1.0f + a.m[5] - a.m[0] - a.m[10]) * 2.0f; out.w = (a.m[8] - a.m[2]) / s; out.x = (a.m[4] + a.m[1]) / s; out.y = 0.25f * s; out.z = (a.m[9] + a.m[6]) / s; } else { s = sqrtf(1.0f + a.m[10] - a.m[0] - a.m[5]) * 2.0f; out.w = (a.m[1] - a.m[4]) / s; out.x = (a.m[8] + a.m[2]) / s; out.y = (a.m[9] + a.m[6]) / s; out.z = 0.25f * s; } return quatNormalize(out); } QuatT quatIdentity(void) { QuatT out = { 0.0f, 0.0f, 0.0f, 1.0f }; return out; } // The inverse rotation: normalised, then conjugated. QuatT quatInverse(QuatT q) { QuatT out = quatNormalize(q); out.x = -out.x; out.y = -out.y; out.z = -out.z; return out; } // The rotation that points -Z along forward with +Y near up. QuatT quatLookRotation(Vec3T forward, Vec3T up) { Mat4T m; Vec3T f = vec3Normalize(forward); Vec3T s; Vec3T u; if (vec3Length(vec3Cross(f, up)) < MATH_EPSILON) { // Looking straight along up: pick any perpendicular. up = (fabsf(f.y) < 0.9f) ? vec3(0.0f, 1.0f, 0.0f) : vec3(0.0f, 0.0f, 1.0f); } s = vec3Normalize(vec3Cross(f, up)); u = vec3Cross(s, f); m = mat4Identity(); m.m[0] = s.x; m.m[1] = s.y; m.m[2] = s.z; m.m[4] = u.x; m.m[5] = u.y; m.m[6] = u.z; m.m[8] = -f.x; m.m[9] = -f.y; m.m[10] = -f.z; return quatFromMat4(m); } // a * b: applies b first, then a. QuatT quatMultiply(QuatT a, QuatT b) { QuatT out; out.x = a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y; out.y = a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x; out.z = a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w; out.w = a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z; return out; } QuatT quatNormalize(QuatT q) { float length = sqrtf(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w); if (length < MATH_EPSILON) { return quatIdentity(); } q.x /= length; q.y /= length; q.z /= length; q.w /= length; return q; } Vec3T quatRotate(QuatT q, Vec3T v) { Vec3T u = vec3(q.x, q.y, q.z); Vec3T t = vec3Scale(vec3Cross(u, v), 2.0f); return vec3Add(vec3Add(v, vec3Scale(t, q.w)), vec3Cross(u, t)); } QuatT quatSlerp(QuatT a, QuatT b, float t) { QuatT out; float cosTheta = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; float theta; float sinTheta; float wa; float wb; // Take the short way round. if (cosTheta < 0.0f) { b.x = -b.x; b.y = -b.y; b.z = -b.z; b.w = -b.w; cosTheta = -cosTheta; } if (cosTheta > 1.0f - MATH_EPSILON) { // Nearly parallel: lerp is accurate and avoids the division. out.x = a.x + (b.x - a.x) * t; out.y = a.y + (b.y - a.y) * t; out.z = a.z + (b.z - a.z) * t; out.w = a.w + (b.w - a.w) * t; return quatNormalize(out); } theta = acosf(cosTheta); sinTheta = sinf(theta); wa = sinf((1.0f - t) * theta) / sinTheta; wb = sinf(t * theta) / sinTheta; out.x = a.x * wa + b.x * wb; out.y = a.y * wa + b.y * wb; out.z = a.z * wa + b.z * wb; out.w = a.w * wa + b.w * wb; return out; } // The inverse of quatFromEuler (Y, then X, then Z). void quatToEuler(QuatT q, float *xDegrees, float *yDegrees, float *zDegrees) { Mat4T m = mat4Compose(vec3(0.0f, 0.0f, 0.0f), q, vec3(1.0f, 1.0f, 1.0f)); float sinX = -m.m[9]; if (sinX > 1.0f) { sinX = 1.0f; } if (sinX < -1.0f) { sinX = -1.0f; } *xDegrees = RADIANS_TO_DEGREES(asinf(sinX)); if (fabsf(sinX) < 1.0f - MATH_EPSILON) { *yDegrees = RADIANS_TO_DEGREES(atan2f(m.m[8], m.m[10])); *zDegrees = RADIANS_TO_DEGREES(atan2f(m.m[1], m.m[5])); } else { // Gimbal lock: give all the twist to Y. *yDegrees = RADIANS_TO_DEGREES(atan2f(-m.m[2], m.m[0])); *zDegrees = 0.0f; } } Vec3T vec3(float x, float y, float z) { Vec3T out = { x, y, z }; return out; } Vec3T vec3Add(Vec3T a, Vec3T b) { return vec3(a.x + b.x, a.y + b.y, a.z + b.z); } Vec3T vec3Cross(Vec3T a, Vec3T b) { return vec3(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x); } float vec3Dot(Vec3T a, Vec3T b) { return a.x * b.x + a.y * b.y + a.z * b.z; } float vec3Length(Vec3T a) { return sqrtf(vec3Dot(a, a)); } Vec3T vec3Lerp(Vec3T a, Vec3T b, float t) { return vec3Add(a, vec3Scale(vec3Subtract(b, a), t)); } Vec3T vec3Normalize(Vec3T a) { float length = vec3Length(a); if (length < MATH_EPSILON) { return vec3(0.0f, 0.0f, 0.0f); } return vec3Scale(a, 1.0f / length); } Vec3T vec3Scale(Vec3T a, float s) { return vec3(a.x * s, a.y * s, a.z * s); } Vec3T vec3Subtract(Vec3T a, Vec3T b) { return vec3(a.x - b.x, a.y - b.y, a.z - b.z); }