diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a635158..36a014a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,11 +19,6 @@ jobs: - uses: treeform/setup-nim-action@v2 with: nim-version: ${{ matrix.nim-version }} - - run: nim r tests/test_quaternion.nim - - run: nim r --gc:arc tests/test_quaternion.nim - - run: nim r -d:vmathObjBased tests/test_quaternion.nim - - run: nim r -d:vmathArrayBased tests/test_quaternion.nim - - run: nim js -r tests/test_quaternion.nim - run: nim r tests/test.nim - run: nim r --gc:arc tests/test.nim - run: nim r -d:vmathObjBased tests/test.nim diff --git a/.gitignore b/.gitignore index ffc0d8a..8f1e202 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ !*.* # normal ignores: -*.js *.exe nimcache +tests/test.js diff --git a/README.md b/README.md index 58b046e..3839e79 100644 --- a/README.md +++ b/README.md @@ -106,21 +106,57 @@ This is the same system used in the GLTF file format. [glTF Spec 2.0](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#coordinate-system-and-units) -## OpenGL matrix column-major notation. - -> [9.005](https://www.opengl.org/archives/resources/faq/technical/transformations.htm) For programming purposes, OpenGL matrices are 16-value arrays with base vectors laid out contiguously in memory. The translation components occupy the 13th, 14th, and 15th elements of the 16-element matrix, where indices are numbered from 1 to 16 as described in section 2.11.2 of the [OpenGL 2.1 Specification](https://registry.khronos.org/OpenGL/specs/gl/glspec21.pdf). -> -> Sadly, the use of column-major format in the spec and blue book has resulted in endless confusion in the OpenGL programming community. Column-major notation suggests that matrices are not laid out in memory as a programmer would expect. - -OpenGL/GLSL/vmath vs Math/Specification notation: -``` - mat4([ - a, b, c, 0, | a d g x | - d, e, f, 0, | b e h y | - g, h, i, 0, | c f i z | - x, y, z, 1 | 0 0 0 1 | - ]) -``` +## How does vmath compare to other libraries? + +vmath follows the standard glTF / OpenGL conventions: right-handed coordinate system, column-major matrix storage, and math-style `[row, col]` indexing. + +We run identical math operations across vmath, GLSL, [nim-glm](https://github.com/nickelsworth/nim-glm), [gl-matrix](https://github.com/toji/gl-matrix), and [Jolt Physics](https://github.com/jrouwe/JoltPhysics) and compare the generated dumps in [conformance/](conformance/). + + +| Feature | vmath | GLSL | nim-GLM | gl-matrix | Jolt Physics | +|----------------------------|:-----:|:----:|:-------:|:---------:|:------------:| +| Vectors | ✅ | ✅ | ✅ | ✅ | ✅ | +| Matrix memory layout | ✅ | ✅ | ✅ | ✅ | ✅ | +| Matrix multiply | ✅ | ✅ | ✅ | ✅ | ✅ | +| Matrix-vector multiply | ✅ | ✅ | ✅ | ✅ | ✅ | +| Rotation matrices | ✅ | ✅ | ✅ | ✅ | ✅ | +| Translation matrices | ✅ | ✅ | ✅ | ✅ | ✅ | +| Scale matrices | ✅ | ✅ | ✅ | ✅ | ✅ | +| Mat2 | ✅ | ✅ | ✅ | ✅ | N/A | +| Mat3 | ✅ | ✅ | ✅ | ✅ | N/A | +| Mat3 2D constructors. | ✅ | ✅ | ✅ | ✅ | N/A | +| Quaternion constructors | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quaternion multiply | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quaternion vector rotation | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quaternion-matrix roundtrip| ✅ | ✅ | ✅ | ✅ | ✅ | +| Quaternion inverse | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quaternion to axis-angle | ✅ | ✅ | ✅ | ✅ | ✅ | +| Matrix inverse | ✅ | ✅ | ✅ | ✅ | ✅ | +| Cross product | ✅ | ✅ | ✅ | ✅ | ✅ | +| Slerp | ✅ | ✅ | ✅ | ✅ | ✅ | +| fromTwoVectors | ✅ | ✅ | ✅ | ✅ | ✅ | +| Quat decomposition (sign) | ✅ | ✅ | ✅ | ✅ | ✅ | +| Scaled-matrix decomposition| ✅ | ✅ | ✅ | ✅ | ✅ | +| LookAt matrix | ✅ | ✅ | ✅ | ✅ | ✅ | +| Perspective matrix | ✅ | ✅ | ✅ | ✅ | ❌ | +| Ortho matrix | ✅ | ✅ | ✅ | ✅ | N/A | +| Euler angle decomposition | ✅ | ✅ | ✅ | N/A | ✅ | +| Element access | ✅ | ✅ | ✅ | N/A | ❌ | + +❌ **Perspective matrix**: Jolt Physics uses Z range `[0, 1]` (Vulkan/DirectX convention) while vmath uses Z range `[-1, 1]` (OpenGL convention). The X and Y scaling match, but Z-related elements differ. + +❌ **Element access note**: Jolt does convention differs from vmath's math-style `[row, col]` interpretation it follows the DirectX/HLSL convention. + +# 2.x.x to 3.0.0 vmath breaking changes: + +Version `3.0.0` changed rotation to be CCW (counter-clockwise) and updated the quaternion conventions to match GLSL, GLM, gl-matrix. Added a multi-library conformance suite. + +* **Rotation matrices (`rotateX`, `rotateY`, `rotateZ`, 2D `rotate`) now use standard CCW (counter-clockwise) convention.** The sin/cos sign placement in the rotation matrices was swapped. If your code relied on the old CW direction, please negate the angle. +* **Quaternion ↔ matrix conversion (`quat()`, `mat4()`) rewritten.** Uses the standard "biggest component" algorithm matching GLM/GLSL. The off-diagonal signs in `mat4(quat)` were swapped to match the new rotation direction. +* **`toAngles` corrected signs and added gimbal-lock clamping.** No longer negates the Y and Z angles to match the new rotation direction. +* **`fromTwoVectors(a, b)` now rotates `a` into `b`.** Previous behavior had that direction reversed. +* **`lookAt()` un-deprecated.** Restored with Y-up which is glTF/OpenGL convention. +* **New functions added:** `quatInverse`, `slerp`, `quatMultiply`, `quatRotate`, `quatRotateX/Y/Z`, `toAngles(quat)`, `mat4(DMat4)`, `dmat4(Mat4)`, and Mat2 operations (`*`, `transpose`, `determinant`, `inverse`). # 1.x.x to 2.0.0 vmath breaking changes: * New right-hand-Z-forward coordinate system and functions that care about diff --git a/conformance/dump_glm.nim b/conformance/dump_glm.nim new file mode 100644 index 0000000..8cb1b1e --- /dev/null +++ b/conformance/dump_glm.nim @@ -0,0 +1,412 @@ +import + std/[math, os, strutils], + glm + +const + OutputPath = parentDir(currentSourcePath()) / "dump_glm.txt" + +proc cleanFloat(value: float32): float32 = + if abs(value) < 0.0000005'f32: + 0'f32 + else: + value + +proc fmt(value: float32): string = + let cleaned = cleanFloat(value) + let sign = if cleaned < 0'f32: "-" else: "+" + + var whole = int(floor(abs(cleaned).float64)) + var frac = int(round((abs(cleaned).float64 - whole.float64) * 1000.0)) + if frac == 1000: + inc whole + frac = 0 + + result = sign + let wholeText = $whole + for _ in wholeText.len ..< 3: + result.add '0' + result.add wholeText + result.add '.' + + let fracText = $frac + for _ in fracText.len ..< 3: + result.add '0' + result.add fracText + +proc appendLine(lines: var seq[string], line = "") = + lines.add line + +proc dumpScalar(lines: var seq[string], label: string, value: float32) = + lines.appendLine(label & ": " & fmt(value)) + +proc dumpVec2(lines: var seq[string], label: string, value: Vec2f) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ">") + +proc dumpVec3(lines: var seq[string], label: string, value: Vec3f) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ">") + +proc dumpVec4(lines: var seq[string], label: string, value: Vec4f) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ", " & fmt(value.w) & ">") + +proc dumpQuat(lines: var seq[string], label: string, value: Quatf) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ", " & fmt(value.w) & ">") + +proc dumpMat2(lines: var seq[string], label: string, value: Mat2f) = + lines.appendLine(label & ":") + let d = cast[array[4, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[2])) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[3])) + lines.appendLine("]") + +proc dumpMat3(lines: var seq[string], label: string, value: Mat3f) = + lines.appendLine(label & ":") + let d = cast[array[9, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[3]) & " " & fmt(d[6])) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[4]) & " " & fmt(d[7])) + lines.appendLine(" " & fmt(d[2]) & " " & fmt(d[5]) & " " & fmt(d[8])) + lines.appendLine("]") + +proc dumpMat4(lines: var seq[string], label: string, value: Mat4f) = + lines.appendLine(label & ":") + let d = cast[array[16, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[4]) & " " & fmt(d[8]) & " " & fmt(d[12]) ) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[5]) & " " & fmt(d[9]) & " " & fmt(d[13]) ) + lines.appendLine(" " & fmt(d[2]) & " " & fmt(d[6]) & " " & fmt(d[10]) & " " & fmt(d[14]) ) + lines.appendLine(" " & fmt(d[3]) & " " & fmt(d[7]) & " " & fmt(d[11]) & " " & fmt(d[15]) ) + lines.appendLine("]") + +proc dumpMat4Default(lines: var seq[string], label: string, value: Mat4f) = + lines.appendLine(label & ": " & $value) + +proc heading(lines: var seq[string], title: string) = + if lines.len > 0: + lines.appendLine() + lines.appendLine("== " & title & " ==") + +proc transformVec2ByMat3(matrix: Mat3f, value: Vec2f): Vec2f = + let r = matrix * vec3f(value.x, value.y, 1'f32) + vec2f(r.x, r.y) + +proc transformVec3ByMat4(matrix: Mat4f, value: Vec3f): Vec3f = + (matrix * vec4f(value, 1'f32)).xyz + +proc transformVec4ByMat4(matrix: Mat4f, value: Vec4f): Vec4f = + matrix * value + +proc rotationOnlyCopy(value: Mat4f): Mat4f = + result = value + result[3] = vec4f(0'f32, 0'f32, 0'f32, 1'f32) + +proc main() = + var lines: seq[string] + + let + angleA = 37'f32 * PI.float32 / 180'f32 + angleB = -23'f32 * PI.float32 / 180'f32 + angleC = 71'f32 * PI.float32 / 180'f32 + + matA = cast[Mat4f]([ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + ]) + matB = cast[Mat4f]([ + -10.0f, -20.0f, -30.0f, -40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, + 90.0f, 100.0f, 110.0f, 120.0f, + 130.0f, 140.0f, 150.0f, 160.0f + ]) + vecA = vec3f(1.25, -2.5, 3.75) + vecB = vec4f(1.25, -2.5, 3.75, 1.0) + + mat2A = cast[Mat2f]([1.0f, 2.0f, 3.0f, 4.0f]) + mat2B = cast[Mat2f]([5.0f, -6.0f, 7.0f, -8.0f]) + vec2A = vec2f(1.25, -2.5) + + mat3A = cast[Mat3f]([1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f]) + mat3B = cast[Mat3f]([-1.0f, 3.0f, 5.0f, 7.0f, -2.0f, 4.0f, 6.0f, 8.0f, -3.0f]) + vec2B = vec2f(3.0, -1.5) + vec3C = vec3f(1.0, -2.0, 3.0) + + rotAngle2D = 45'f32 * PI.float32 / 180'f32 + + scaleM = mat4f(1).scale(2'f32, 3'f32, 4'f32) + translateM = mat4f(1).translate(10'f32, 20'f32, 30'f32) + rotateXM = mat4f(1).rotateX(angleA) + rotateYM = mat4f(1).rotateY(angleB) + rotateZM = mat4f(1).rotateZ(angleC) + pureRotationM = rotateZM * rotateYM * rotateXM + + axis = normalize(vec3f(1.0, 2.0, -3.0)) + axisAngle = 48'f32 * PI.float32 / 180'f32 + axisQuat = quatf(axis, axisAngle) + axisMat = axisQuat.mat4() + transformM = translateM * rotateZM * rotateYM * rotateXM * scaleM + + quatX = quatf(vec3f(1'f32, 0'f32, 0'f32), angleA) + quatY = quatf(vec3f(0'f32, 1'f32, 0'f32), angleB) + quatZ = quatf(vec3f(0'f32, 0'f32, 1'f32), angleC) + quatXY = quatX * quatY + quatXYZ = quatXY * quatZ + + hardAxis = normalize(vec3f(1.0, -2.0, 3.0)) + hardAngle = 170'f32 * PI.float32 / 180'f32 + hardQuat = quatf(hardAxis, hardAngle) + hardMat = hardQuat.mat4() + + rotationOnlyM = rotationOnlyCopy(transformM) + + lines.heading("dump") + lines.appendLine("notes: matrices are printed in common column-major order") + + lines.heading("matrix basics") + lines.dumpMat4("identity", mat4f()) + lines.dumpMat4("matrix_a", matA) + lines.dumpMat4("matrix_b", matB) + + lines.heading("matrix multiply") + lines.dumpMat4("matrix_a * matrix_b", matA * matB) + lines.dumpMat4("matrix_b * matrix_a", matB * matA) + + lines.heading("element access [row, col]") + lines.dumpScalar("transform[0, 0]", matA[0, 0]) + lines.dumpScalar("transform[0, 1]", matA[0, 1]) + lines.dumpScalar("transform[0, 2]", matA[0, 2]) + lines.dumpScalar("transform[0, 3]", matA[0, 3]) + lines.dumpScalar("transform[1, 0]", matA[1, 0]) + lines.dumpScalar("transform[1, 1]", matA[1, 1]) + lines.dumpScalar("transform[1, 2]", matA[1, 2]) + lines.dumpScalar("transform[1, 3]", matA[1, 3]) + lines.dumpScalar("transform[2, 0]", matA[2, 0]) + lines.dumpScalar("transform[2, 1]", matA[2, 1]) + lines.dumpScalar("transform[2, 2]", matA[2, 2]) + lines.dumpScalar("transform[2, 3]", matA[2, 3]) + lines.dumpScalar("transform[3, 0]", matA[3, 0]) + lines.dumpScalar("transform[3, 1]", matA[3, 1]) + lines.dumpScalar("transform[3, 2]", matA[3, 2]) + lines.dumpScalar("transform[3, 3]", matA[3, 3]) + + lines.heading("mat2 basics") + lines.dumpMat2("identity", mat2f()) + lines.dumpMat2("mat2_a", mat2A) + lines.dumpMat2("mat2_b", mat2B) + + lines.heading("mat2 multiply") + lines.dumpMat2("mat2_a * mat2_b", mat2A * mat2B) + lines.dumpMat2("mat2_b * mat2_a", mat2B * mat2A) + + lines.heading("mat2 element access [row, col]") + lines.dumpScalar("mat2[0, 0]", mat2A[0, 0]) + lines.dumpScalar("mat2[0, 1]", mat2A[0, 1]) + lines.dumpScalar("mat2[1, 0]", mat2A[1, 0]) + lines.dumpScalar("mat2[1, 1]", mat2A[1, 1]) + + lines.heading("mat2 vector multiply") + lines.dumpVec2("vec2_input", vec2A) + lines.dumpVec2("mat2_a * vec2", mat2A * vec2A) + + lines.heading("mat2 transpose") + lines.dumpMat2("mat2_a.transpose", transpose(mat2A)) + + lines.heading("mat2 inverse") + lines.dumpMat2("mat2_a.inverse", inverse(mat2A)) + + lines.heading("mat3 basics") + lines.dumpMat3("identity", mat3f()) + lines.dumpMat3("mat3_a", mat3A) + lines.dumpMat3("mat3_b", mat3B) + + lines.heading("mat3 multiply") + lines.dumpMat3("mat3_a * mat3_b", mat3A * mat3B) + lines.dumpMat3("mat3_b * mat3_a", mat3B * mat3A) + + lines.heading("mat3 element access [row, col]") + lines.dumpScalar("mat3[0, 0]", mat3A[0, 0]) + lines.dumpScalar("mat3[0, 1]", mat3A[0, 1]) + lines.dumpScalar("mat3[0, 2]", mat3A[0, 2]) + lines.dumpScalar("mat3[1, 0]", mat3A[1, 0]) + lines.dumpScalar("mat3[1, 1]", mat3A[1, 1]) + lines.dumpScalar("mat3[1, 2]", mat3A[1, 2]) + lines.dumpScalar("mat3[2, 0]", mat3A[2, 0]) + lines.dumpScalar("mat3[2, 1]", mat3A[2, 1]) + lines.dumpScalar("mat3[2, 2]", mat3A[2, 2]) + + lines.heading("mat3 vector multiply") + lines.dumpVec2("vec2_input", vec2B) + lines.dumpVec3("vec3_input", vec3C) + lines.dumpVec2("mat3_a * vec2", transformVec2ByMat3(mat3A, vec2B)) + lines.dumpVec3("mat3_a * vec3", mat3A * vec3C) + + lines.heading("mat3 transpose") + lines.dumpMat3("mat3_a.transpose", transpose(mat3A)) + + lines.heading("mat3 inverse") + lines.dumpMat3("mat3_a.inverse", inverse(mat3A)) + + lines.heading("mat3 constructors") + block: + let + s = mat3f(1'f32) + var scale2D = s + scale2D[0, 0] = 2'f32 + scale2D[1, 1] = 3'f32 + lines.dumpMat3("scale2d", scale2D) + + var translate2D = mat3f(1'f32) + translate2D[2, 0] = 5'f32 + translate2D[2, 1] = 10'f32 + lines.dumpMat3("translate2d", translate2D) + + lines.dumpScalar("rotate_angle_radians", rotAngle2D) + let c = cos(rotAngle2D) + let s2 = sin(rotAngle2D) + var rotate2D = mat3f(1'f32) + rotate2D[0, 0] = c + rotate2D[0, 1] = s2 + rotate2D[1, 0] = -s2 + rotate2D[1, 1] = c + lines.dumpMat3("rotate2d", rotate2D) + lines.dumpMat3("translate * rotate * scale", translate2D * rotate2D * scale2D) + + lines.heading("matrix constructors and composition") + lines.dumpMat4("scale", scaleM) + lines.dumpMat4("translate", translateM) + lines.dumpMat4("rotate_x", rotateXM) + lines.dumpMat4("rotate_y", rotateYM) + lines.dumpMat4("rotate_z", rotateZM) + lines.dumpMat4("pure_rotation = rotate_z * rotate_y * rotate_x", pureRotationM) + lines.dumpMat4("transform = translate * rotate_z * rotate_y * rotate_x * scale", transformM) + + lines.heading("matrix vector multiply") + lines.dumpVec3("vec3_input", vecA) + lines.dumpVec4("vec4_input", vecB) + lines.dumpVec3("transform * vec3", transformVec3ByMat4(transformM, vecA)) + lines.dumpVec4("transform * vec4", transformVec4ByMat4(transformM, vecB)) + lines.dumpVec3("rotate_z * vec3", transformVec3ByMat4(rotateZM, vecA)) + lines.dumpVec3("translate * vec3", transformVec3ByMat4(translateM, vecA)) + + lines.heading("quaternion constructors") + lines.dumpQuat("quat_identity", quatf()) + lines.dumpQuat("quat_rotate_x", quatX) + lines.dumpQuat("quat_rotate_y", quatY) + lines.dumpQuat("quat_rotate_z", quatZ) + lines.dumpVec3("axis_normalized", axis) + lines.dumpScalar("axis_angle_radians", axisAngle) + lines.dumpQuat("from_axis_angle", axisQuat) + lines.dumpMat4("from_axis_angle.mat4", axisMat) + + lines.heading("quaternion multiply") + lines.dumpQuat("quat_x", quatX) + lines.dumpQuat("quat_y", quatY) + lines.dumpQuat("quat_z", quatZ) + lines.dumpQuat("quat_multiply(quat_x, quat_y)", quatXY) + lines.dumpQuat("quat_multiply(quat_multiply(quat_x, quat_y), quat_z)", quatXYZ) + lines.dumpMat4("quat_xy.mat4", quatXY.mat4()) + lines.dumpMat4("quat_xyz.mat4", quatXYZ.mat4()) + + lines.heading("quaternion vector rotate") + lines.dumpVec3("input", vecA) + lines.dumpVec3("quat_rotate(quat_x, input)", quatX * vecA) + lines.dumpVec3("quat_rotate(quat_y, input)", quatY * vecA) + lines.dumpVec3("quat_rotate(quat_z, input)", quatZ * vecA) + lines.dumpVec3("quat_rotate(from_axis_angle, input)", axisQuat * vecA) + lines.dumpVec3("quat_z * input", quatZ * vecA) + + lines.heading("matrix quaternion roundtrip") + let pureRotationQuat = quat(pureRotationM) + lines.dumpQuat("pure_rotation.quat", pureRotationQuat) + lines.dumpMat4("pure_rotation", pureRotationM) + lines.dumpMat4("pure_rotation.quat.mat4", pureRotationQuat.mat4()) + lines.dumpMat4("transform.rotation_only", rotationOnlyM) + lines.dumpQuat("transform.rotation_only.quat", quat(rotationOnlyM)) + let axisMatQuat = quat(axisMat) + lines.dumpQuat("axis_mat.quat", axisMatQuat) + lines.dumpMat4("axis_mat.quat.mat4", axisMatQuat.mat4()) + lines.appendLine("hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero") + lines.dumpQuat("hard_decomp.quat_original", hardQuat) + lines.dumpQuat("hard_decomp.quat_from_mat", quat(hardMat)) + lines.dumpMat4("hard_decomp.mat4", hardMat) + lines.dumpMat4("hard_decomp.quat_from_mat.mat4", quat(hardMat).mat4()) + + lines.heading("lookAt matrix") + lines.appendLine("notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0)") + lines.dumpMat4("lookAt", lookAtRH(vec3f(5, 5, 5), vec3f(0, 0, 0), vec3f(0, 1, 0))) + + lines.heading("euler angle decomposition") + lines.appendLine("notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians") + let pureRotAngles = eulerAngles(quat(pureRotationM)) + let axisAngles = eulerAngles(axisQuat) + lines.dumpVec3("pure_rotation.quat.euler", pureRotAngles) + lines.dumpVec3("from_axis_angle.euler", axisAngles) + + lines.heading("matrix inverse") + lines.dumpMat4("transform.inverse", inverse(transformM)) + lines.dumpMat4("pure_rotation.inverse", inverse(pureRotationM)) + + lines.heading("cross product") + lines.appendLine("notes: cross(a, b) where a and b are vec3") + let crossA = vec3f(1, 0, 0) + let crossB = vec3f(0, 1, 0) + let crossC = normalize(vec3f(1, 2, 3)) + let crossD = normalize(vec3f(-1, 0.5, 2)) + lines.dumpVec3("cross(x_axis, y_axis)", cross(crossA, crossB)) + lines.dumpVec3("cross(y_axis, x_axis)", cross(crossB, crossA)) + lines.dumpVec3("cross(c, d)", cross(crossC, crossD)) + + lines.heading("slerp") + lines.appendLine("notes: slerp(a, b, t) between quat_x and quat_z") + lines.dumpQuat("slerp(quat_x, quat_z, 0.25)", slerp(quatX, quatZ, 0.25'f32)) + lines.dumpQuat("slerp(quat_x, quat_z, 0.5)", slerp(quatX, quatZ, 0.5'f32)) + lines.dumpQuat("slerp(quat_x, quat_z, 0.75)", slerp(quatX, quatZ, 0.75'f32)) + + lines.heading("fromTwoVectors") + lines.appendLine("notes: quaternion that rotates vector a to vector b") + proc fromTwoVectors(a, b: Vec3f): Quatf = + let d = dot(a, b) + let c = cross(a, b) + let w = sqrt(dot(a, a) * dot(b, b)) + d + result = quatf(c.x, c.y, c.z, w) + result = normalize(result) + let fromA = vec3f(1, 0, 0) + let fromB = vec3f(0, 1, 0) + let fromC = normalize(vec3f(1, 2, -1)) + let fromD = normalize(vec3f(-1, 0.5, 2)) + lines.dumpQuat("from_x_to_y", fromTwoVectors(fromA, fromB)) + lines.dumpQuat("from_c_to_d", fromTwoVectors(fromC, fromD)) + lines.dumpVec3("verify_x_to_y", fromTwoVectors(fromA, fromB) * fromA) + lines.dumpVec3("verify_c_to_d", fromTwoVectors(fromC, fromD) * fromC) + + lines.heading("quaternion inverse") + lines.dumpQuat("from_axis_angle.inverse", inverse(axisQuat)) + lines.dumpQuat("verify_q_mul_qinv", axisQuat * inverse(axisQuat)) + + lines.heading("quaternion to axis-angle") + lines.dumpVec3("from_axis_angle.axis", axis(axisQuat)) + lines.dumpScalar("from_axis_angle.angle", angle(axisQuat)) + lines.dumpVec3("quat_xyz.axis", axis(quatXYZ)) + lines.dumpScalar("quat_xyz.angle", angle(quatXYZ)) + + lines.heading("perspective matrix") + lines.appendLine("notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0") + let fovyRad = 60'f32 * PI.float32 / 180'f32 + lines.dumpMat4("perspective", perspectiveRH(fovyRad, 1.5'f32, 0.1'f32, 100'f32)) + + lines.heading("ortho matrix") + lines.appendLine("notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0") + lines.dumpMat4("ortho", ortho(-10'f32, 10'f32, -7.5'f32, 7.5'f32, 0.1'f32, 100'f32)) + + lines.heading("basis directions") + lines.appendLine("N/A") + + lines.heading("default matrix printer") + lines.dumpMat4Default("matrix_a.default", matA) + lines.dumpMat4Default("transform.default", transformM) + + writeFile(OutputPath, lines.join("\n") & "\n") + echo "Wrote ", OutputPath + +main() diff --git a/conformance/dump_glm.txt b/conformance/dump_glm.txt new file mode 100644 index 0000000..d405806 --- /dev/null +++ b/conformance/dump_glm.txt @@ -0,0 +1,455 @@ +== dump == +notes: matrices are printed in common column-major order + +== matrix basics == +identity: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +matrix_a: +[ + +001.000 +005.000 +009.000 +013.000 + +002.000 +006.000 +010.000 +014.000 + +003.000 +007.000 +011.000 +015.000 + +004.000 +008.000 +012.000 +016.000 +] +matrix_b: +[ + -010.000 +050.000 +090.000 +130.000 + -020.000 +060.000 +100.000 +140.000 + -030.000 +070.000 +110.000 +150.000 + -040.000 +080.000 +120.000 +160.000 +] + +== matrix multiply == +matrix_a * matrix_b: +[ + -900.000 +2020.000 +3140.000 +4260.000 + -1000.000 +2280.000 +3560.000 +4840.000 + -1100.000 +2540.000 +3980.000 +5420.000 + -1200.000 +2800.000 +4400.000 +6000.000 +] +matrix_b * matrix_a: +[ + +880.000 +1920.000 +2960.000 +4000.000 + +960.000 +2080.000 +3200.000 +4320.000 + +1040.000 +2240.000 +3440.000 +4640.000 + +1120.000 +2400.000 +3680.000 +4960.000 +] + +== element access [row, col] == +transform[0, 0]: +001.000 +transform[0, 1]: +002.000 +transform[0, 2]: +003.000 +transform[0, 3]: +004.000 +transform[1, 0]: +005.000 +transform[1, 1]: +006.000 +transform[1, 2]: +007.000 +transform[1, 3]: +008.000 +transform[2, 0]: +009.000 +transform[2, 1]: +010.000 +transform[2, 2]: +011.000 +transform[2, 3]: +012.000 +transform[3, 0]: +013.000 +transform[3, 1]: +014.000 +transform[3, 2]: +015.000 +transform[3, 3]: +016.000 + +== mat2 basics == +identity: +[ + +001.000 +000.000 + +000.000 +001.000 +] +mat2_a: +[ + +001.000 +003.000 + +002.000 +004.000 +] +mat2_b: +[ + +005.000 +007.000 + -006.000 -008.000 +] + +== mat2 multiply == +mat2_a * mat2_b: +[ + -013.000 -017.000 + -014.000 -018.000 +] +mat2_b * mat2_a: +[ + +019.000 +043.000 + -022.000 -050.000 +] + +== mat2 element access [row, col] == +mat2[0, 0]: +001.000 +mat2[0, 1]: +002.000 +mat2[1, 0]: +003.000 +mat2[1, 1]: +004.000 + +== mat2 vector multiply == +vec2_input: <+001.250, -002.500> +mat2_a * vec2: <-006.250, -007.500> + +== mat2 transpose == +mat2_a.transpose: +[ + +001.000 +002.000 + +003.000 +004.000 +] + +== mat2 inverse == +mat2_a.inverse: +[ + -002.000 +001.500 + +001.000 -000.500 +] + +== mat3 basics == +identity: +[ + +001.000 +000.000 +000.000 + +000.000 +001.000 +000.000 + +000.000 +000.000 +001.000 +] +mat3_a: +[ + +001.000 +004.000 +007.000 + +002.000 +005.000 +008.000 + +003.000 +006.000 +010.000 +] +mat3_b: +[ + -001.000 +007.000 +006.000 + +003.000 -002.000 +008.000 + +005.000 +004.000 -003.000 +] + +== mat3 multiply == +mat3_a * mat3_b: +[ + +046.000 +027.000 +017.000 + +053.000 +036.000 +028.000 + +065.000 +049.000 +036.000 +] +mat3_b * mat3_a: +[ + +031.000 +067.000 +109.000 + +023.000 +050.000 +085.000 + +004.000 +022.000 +037.000 +] + +== mat3 element access [row, col] == +mat3[0, 0]: +001.000 +mat3[0, 1]: +002.000 +mat3[0, 2]: +003.000 +mat3[1, 0]: +004.000 +mat3[1, 1]: +005.000 +mat3[1, 2]: +006.000 +mat3[2, 0]: +007.000 +mat3[2, 1]: +008.000 +mat3[2, 2]: +010.000 + +== mat3 vector multiply == +vec2_input: <+003.000, -001.500> +vec3_input: <+001.000, -002.000, +003.000> +mat3_a * vec2: <+004.000, +006.500> +mat3_a * vec3: <+014.000, +016.000, +021.000> + +== mat3 transpose == +mat3_a.transpose: +[ + +001.000 +002.000 +003.000 + +004.000 +005.000 +006.000 + +007.000 +008.000 +010.000 +] + +== mat3 inverse == +mat3_a.inverse: +[ + -000.667 -000.667 +001.000 + -001.333 +003.667 -002.000 + +001.000 -002.000 +001.000 +] + +== mat3 constructors == +scale2d: +[ + +002.000 +000.000 +000.000 + +000.000 +003.000 +000.000 + +000.000 +000.000 +001.000 +] +translate2d: +[ + +001.000 +000.000 +005.000 + +000.000 +001.000 +010.000 + +000.000 +000.000 +001.000 +] +rotate_angle_radians: +000.785 +rotate2d: +[ + +000.707 -000.707 +000.000 + +000.707 +000.707 +000.000 + +000.000 +000.000 +001.000 +] +translate * rotate * scale: +[ + +001.414 -002.121 +005.000 + +001.414 +002.121 +010.000 + +000.000 +000.000 +001.000 +] + +== matrix constructors and composition == +scale: +[ + +002.000 +000.000 +000.000 +000.000 + +000.000 +003.000 +000.000 +000.000 + +000.000 +000.000 +004.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +translate: +[ + +001.000 +000.000 +000.000 +010.000 + +000.000 +001.000 +000.000 +020.000 + +000.000 +000.000 +001.000 +030.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_x: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +000.799 -000.602 +000.000 + +000.000 +000.602 +000.799 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_y: +[ + +000.921 +000.000 -000.391 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.391 +000.000 +000.921 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_z: +[ + +000.326 -000.946 +000.000 +000.000 + +000.946 +000.326 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation = rotate_z * rotate_y * rotate_x: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform = translate * rotate_z * rotate_y * rotate_x * scale: +[ + +000.599 -002.495 +001.870 +010.000 + +001.741 +000.113 -001.964 +020.000 + +000.781 +001.662 +002.941 +030.000 + +000.000 +000.000 +000.000 +001.000 +] + +== matrix vector multiply == +vec3_input: <+001.250, -002.500, +003.750> +vec4_input: <+001.250, -002.500, +003.750, +001.000> +transform * vec3: <+023.998, +014.529, +037.849> +transform * vec4: <+023.998, +014.529, +037.849, +001.000> +rotate_z * vec3: <+002.771, +000.368, +003.750> +translate * vec3: <+011.250, +017.500, +033.750> + +== quaternion constructors == +quat_identity: <+000.000, +000.000, +000.000, +001.000> +quat_rotate_x: <+000.317, +000.000, +000.000, +000.948> +quat_rotate_y: <+000.000, -000.199, +000.000, +000.980> +quat_rotate_z: <+000.000, +000.000, +000.581, +000.814> +axis_normalized: <+000.267, +000.535, -000.802> +axis_angle_radians: +000.838 +from_axis_angle: <+000.109, +000.217, -000.326, +000.914> +from_axis_angle.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion multiply == +quat_x: <+000.317, +000.000, +000.000, +000.948> +quat_y: <+000.000, -000.199, +000.000, +000.980> +quat_z: <+000.000, +000.000, +000.581, +000.814> +quat_multiply(quat_x, quat_y): <+000.311, -000.189, -000.063, +000.929> +quat_multiply(quat_multiply(quat_x, quat_y), quat_z): <+000.143, -000.334, +000.488, +000.793> +quat_xy.mat4: +[ + +000.921 +000.000 -000.391 +000.000 + -000.235 +000.799 -000.554 +000.000 + +000.312 +000.602 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +quat_xyz.mat4: +[ + +000.300 -000.870 -000.391 +000.000 + +000.679 +000.482 -000.554 +000.000 + +000.671 -000.099 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion vector rotate == +input: <+001.250, -002.500, +003.750> +quat_rotate(quat_x, input): <+001.250, -004.253, +001.490> +quat_rotate(quat_y, input): <-000.315, -002.500, +003.940> +quat_rotate(quat_z, input): <+002.771, +000.368, +003.750> +quat_rotate(from_axis_angle, input): <+000.482, -003.871, +002.580> +quat_z * input: <+002.771, +000.368, +003.750> + +== matrix quaternion roundtrip == +pure_rotation.quat: <+000.363, +000.027, +000.591, +000.720> +pure_rotation: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.quat.mat4: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only: +[ + +000.599 -002.495 +001.870 +000.000 + +001.741 +000.113 -001.964 +000.000 + +000.781 +001.662 +002.941 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only.quat: <+000.840, +000.252, +000.982, +001.079> +axis_mat.quat: <+000.109, +000.217, -000.326, +000.914> +axis_mat.quat.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero +hard_decomp.quat_original: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.quat_from_mat: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.quat_from_mat.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== lookAt matrix == +notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0) +lookAt: +[ + +000.707 +000.000 -000.707 +000.000 + -000.408 +000.816 -000.408 +000.000 + +000.577 +000.577 +000.577 -008.660 + +000.000 +000.000 +000.000 +001.000 +] + +== euler angle decomposition == +notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians +pure_rotation.quat.euler: <+000.646, -000.401, +001.239> +from_axis_angle.euler: <+000.064, +000.487, -000.670> + +== matrix inverse == +transform.inverse: +[ + +000.150 +000.435 +000.195 -016.063 + -000.277 +000.013 +000.185 -003.019 + +000.117 -000.123 +000.184 -004.227 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.inverse: +[ + +000.300 +000.870 +000.391 +000.000 + -000.832 +000.038 +000.554 +000.000 + +000.467 -000.491 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== cross product == +notes: cross(a, b) where a and b are vec3 +cross(x_axis, y_axis): <+000.000, +000.000, +001.000> +cross(y_axis, x_axis): <+000.000, +000.000, -001.000> +cross(c, d): <+000.292, -000.583, +000.292> + +== slerp == +notes: slerp(a, b, t) between quat_x and quat_z +slerp(quat_x, quat_z, 0.25): <+000.247, +000.000, +000.157, +000.956> +slerp(quat_x, quat_z, 0.5): <+000.169, +000.000, +000.308, +000.936> +slerp(quat_x, quat_z, 0.75): <+000.086, +000.000, +000.451, +000.888> + +== fromTwoVectors == +notes: quaternion that rotates vector a to vector b +from_x_to_y: <+000.000, +000.000, +000.707, +000.707> +from_c_to_d: <+000.707, -000.157, +000.393, +000.567> +verify_x_to_y: <+000.000, +001.000, +000.000> +verify_c_to_d: <-000.436, +000.218, +000.873> + +== quaternion inverse == +from_axis_angle.inverse: <-000.109, -000.217, +000.326, +000.914> +verify_q_mul_qinv: <+000.000, +000.000, +000.000, +001.000> + +== quaternion to axis-angle == +from_axis_angle.axis: <+000.267, +000.535, -000.802> +from_axis_angle.angle: +000.838 +quat_xyz.axis: <+000.235, -000.549, +000.802> +quat_xyz.angle: +001.309 + +== perspective matrix == +notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0 +perspective: +[ + +001.155 +000.000 +000.000 +000.000 + +000.000 +001.732 +000.000 +000.000 + +000.000 +000.000 -001.002 -000.200 + +000.000 +000.000 -001.000 +000.000 +] + +== ortho matrix == +notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0 +ortho: +[ + +000.100 +000.000 +000.000 +000.000 + +000.000 +000.133 +000.000 +000.000 + +000.000 +000.000 -000.020 -001.002 + +000.000 +000.000 +000.000 +001.000 +] + +== basis directions == +N/A + +== default matrix printer == +matrix_a.default: / 1.0 5.0 9.0 13.0 \ +| 2.0 6.0 10.0 14.0 | +| 3.0 7.0 11.0 15.0 | + \ 4.0 8.0 12.0 16.0 / + +transform.default: / 0.5993741 -2.4950442 1.8697325 10.0 \ +| 1.7407088 0.11302096 -1.9639301 20.0 | +| 0.78146225 1.6619208 2.9405916 30.0 | + \ 0.0 0.0 0.0 1.0 / + diff --git a/conformance/dump_glmatrix.js b/conformance/dump_glmatrix.js new file mode 100644 index 0000000..83073db --- /dev/null +++ b/conformance/dump_glmatrix.js @@ -0,0 +1,436 @@ +import * as mat2 from "../../gl-matrix/src/mat2.js"; +import * as mat3 from "../../gl-matrix/src/mat3.js"; +import * as mat4 from "../../gl-matrix/src/mat4.js"; +import * as vec2 from "../../gl-matrix/src/vec2.js"; +import * as vec3 from "../../gl-matrix/src/vec3.js"; +import * as vec4 from "../../gl-matrix/src/vec4.js"; +import * as quat from "../../gl-matrix/src/quat.js"; +import { writeFileSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const OutputPath = join(__dirname, "dump_glmatrix.txt"); + +function cleanFloat(value) { + if (Math.abs(value) < 0.0000005) return 0; + return value; +} + +function fmt(value) { + const cleaned = cleanFloat(value); + const sign = cleaned < 0 ? "-" : "+"; + let abs = Math.abs(cleaned); + let whole = Math.floor(abs); + let frac = Math.round((abs - whole) * 1000); + if (frac === 1000) { + whole++; + frac = 0; + } + return sign + String(whole).padStart(3, "0") + "." + String(frac).padStart(3, "0"); +} + +const lines = []; +function appendLine(line = "") { + lines.push(line); +} +function heading(title) { + if (lines.length > 0) appendLine(); + appendLine("== " + title + " =="); +} +function dumpScalar(label, value) { + appendLine(label + ": " + fmt(value)); +} +function dumpVec2(label, v) { + appendLine(label + ": <" + fmt(v[0]) + ", " + fmt(v[1]) + ">"); +} +function dumpVec3(label, v) { + appendLine(label + ": <" + fmt(v[0]) + ", " + fmt(v[1]) + ", " + fmt(v[2]) + ">"); +} +function dumpVec4(label, v) { + appendLine(label + ": <" + fmt(v[0]) + ", " + fmt(v[1]) + ", " + fmt(v[2]) + ", " + fmt(v[3]) + ">"); +} +function dumpQuat(label, q) { + appendLine(label + ": <" + fmt(q[0]) + ", " + fmt(q[1]) + ", " + fmt(q[2]) + ", " + fmt(q[3]) + ">"); +} +function dumpMat2(label, m) { + appendLine(label + ":"); + appendLine("["); + appendLine(" " + fmt(m[0]) + " " + fmt(m[2])); + appendLine(" " + fmt(m[1]) + " " + fmt(m[3])); + appendLine("]"); +} +function dumpMat3(label, m) { + appendLine(label + ":"); + appendLine("["); + appendLine(" " + fmt(m[0]) + " " + fmt(m[3]) + " " + fmt(m[6])); + appendLine(" " + fmt(m[1]) + " " + fmt(m[4]) + " " + fmt(m[7])); + appendLine(" " + fmt(m[2]) + " " + fmt(m[5]) + " " + fmt(m[8])); + appendLine("]"); +} +function dumpMat4(label, m) { + appendLine(label + ":"); + appendLine("["); + appendLine(" " + fmt(m[0]) + " " + fmt(m[4]) + " " + fmt(m[8]) + " " + fmt(m[12])); + appendLine(" " + fmt(m[1]) + " " + fmt(m[5]) + " " + fmt(m[9]) + " " + fmt(m[13])); + appendLine(" " + fmt(m[2]) + " " + fmt(m[6]) + " " + fmt(m[10]) + " " + fmt(m[14])); + appendLine(" " + fmt(m[3]) + " " + fmt(m[7]) + " " + fmt(m[11]) + " " + fmt(m[15])); + appendLine("]"); +} +function dumpMat4Default(label, m) { + appendLine(label + ": " + mat4.str(m)); +} + +function scaleMat(sx, sy, sz) { + return mat4.scale(mat4.create(), mat4.create(), vec3.fromValues(sx, sy, sz)); +} +function translateMat(tx, ty, tz) { + return mat4.translate(mat4.create(), mat4.create(), vec3.fromValues(tx, ty, tz)); +} +function rotateXMat(angle) { + return mat4.rotateX(mat4.create(), mat4.create(), angle); +} +function rotateYMat(angle) { + return mat4.rotateY(mat4.create(), mat4.create(), angle); +} +function rotateZMat(angle) { + return mat4.rotateZ(mat4.create(), mat4.create(), angle); +} +function mul(a, b) { + return mat4.multiply(mat4.create(), a, b); +} +function transformVec3(m, v) { + return vec3.transformMat4(vec3.create(), v, m); +} +function transformVec4(m, v) { + return vec4.transformMat4(vec4.create(), v, m); +} +function quatFromAxisAngle(axis, angle) { + return quat.setAxisAngle(quat.create(), axis, angle); +} +function quatMul(a, b) { + return quat.multiply(quat.create(), a, b); +} +function quatRotateVec3(q, v) { + return vec3.transformQuat(vec3.create(), v, q); +} +function mat4FromQuat(q) { + return mat4.fromQuat(mat4.create(), q); +} +function rotationOnlyCopy(m) { + const out = mat4.clone(m); + out[12] = 0; + out[13] = 0; + out[14] = 0; + out[15] = 1; + return out; +} +function quatFromMat(m) { + return quat.fromMat3(quat.create(), mat3.fromMat4(mat3.create(), m)); +} +function quatToEulerXYZ(q) { + const x = q[0]; + const y = q[1]; + const z = q[2]; + const w = q[3]; + + const ex = Math.atan2(2 * (w * x + y * z), 1 - 2 * (x * x + y * y)); + const ey = Math.asin(Math.max(-1, Math.min(1, 2 * (w * y - z * x)))); + const ez = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)); + return vec3.fromValues(ex, ey, ez); +} +function basisRight(m) { + return vec3.fromValues(m[0], m[1], m[2]); +} +function basisUp(m) { + return vec3.fromValues(m[4], m[5], m[6]); +} +function basisForward(m) { + return vec3.fromValues(m[8], m[9], m[10]); +} +function quatToAxisAngle(q) { + const normalized = quat.normalize(quat.create(), q); + const w = Math.max(-1, Math.min(1, normalized[3])); + const angle = 2 * Math.acos(w); + const s = Math.sqrt(Math.max(0, 1 - w * w)); + if (s < 0.00001) { + return { + axis: vec3.fromValues(1, 0, 0), + angle: 0, + }; + } + return { + axis: vec3.fromValues(normalized[0] / s, normalized[1] / s, normalized[2] / s), + angle, + }; +} + +const DEG2RAD = Math.PI / 180; +const angleA = 37 * DEG2RAD; +const angleB = -23 * DEG2RAD; +const angleC = 71 * DEG2RAD; + +const matA = new Float32Array([ + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 +]); +const matB = new Float32Array([ + -10.0, -20.0, -30.0, -40.0, + 50.0, 60.0, 70.0, 80.0, + 90.0, 100.0, 110.0, 120.0, + 130.0, 140.0, 150.0, 160.0 +]); +const vecA = vec3.fromValues(1.25, -2.5, 3.75); +const vecB = vec4.fromValues(1.25, -2.5, 3.75, 1.0); + +const mat2A = new Float32Array([1.0, 2.0, 3.0, 4.0]); +const mat2B = new Float32Array([5.0, -6.0, 7.0, -8.0]); +const vec2A = vec2.fromValues(1.25, -2.5); + +const mat3A = new Float32Array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 10.0]); +const mat3B = new Float32Array([-1.0, 3.0, 5.0, 7.0, -2.0, 4.0, 6.0, 8.0, -3.0]); +const vec2B = vec2.fromValues(3.0, -1.5); +const vec3C = vec3.fromValues(1.0, -2.0, 3.0); + +const rotAngle2D = 45 * DEG2RAD; + +const scaleM = scaleMat(2.0, 3.0, 4.0); +const translateM = translateMat(10.0, 20.0, 30.0); +const rotateXM = rotateXMat(angleA); +const rotateYM = rotateYMat(angleB); +const rotateZM = rotateZMat(angleC); +const pureRotationM = mul(rotateZM, mul(rotateYM, rotateXM)); + +const axis = vec3.normalize(vec3.create(), vec3.fromValues(1.0, 2.0, -3.0)); +const axisAngle = 48 * DEG2RAD; +const axisQuat = quatFromAxisAngle(axis, axisAngle); +const axisMat = mat4FromQuat(axisQuat); +const transformM = mul(translateM, mul(rotateZM, mul(rotateYM, mul(rotateXM, scaleM)))); + +const quatX = quatFromAxisAngle(vec3.fromValues(1, 0, 0), angleA); +const quatY = quatFromAxisAngle(vec3.fromValues(0, 1, 0), angleB); +const quatZ = quatFromAxisAngle(vec3.fromValues(0, 0, 1), angleC); +const quatXY = quatMul(quatX, quatY); +const quatXYZ = quatMul(quatXY, quatZ); + +const hardAxis = vec3.normalize(vec3.create(), vec3.fromValues(1.0, -2.0, 3.0)); +const hardAngle = 170 * DEG2RAD; +const hardQuat = quatFromAxisAngle(hardAxis, hardAngle); +const hardMat = mat4FromQuat(hardQuat); + +const rotationOnlyM = rotationOnlyCopy(transformM); +const pureRotationQuat = quatFromMat(pureRotationM); + +heading("dump"); +appendLine("notes: matrices are printed in common column-major order"); + +heading("matrix basics"); +dumpMat4("identity", mat4.create()); +dumpMat4("matrix_a", matA); +dumpMat4("matrix_b", matB); + +heading("matrix multiply"); +dumpMat4("matrix_a * matrix_b", mul(matA, matB)); +dumpMat4("matrix_b * matrix_a", mul(matB, matA)); + +heading("element access [row, col]"); +appendLine("N/A"); + +heading("mat2 basics"); +dumpMat2("identity", mat2.create()); +dumpMat2("mat2_a", mat2A); +dumpMat2("mat2_b", mat2B); + +heading("mat2 multiply"); +dumpMat2("mat2_a * mat2_b", mat2.multiply(mat2.create(), mat2A, mat2B)); +dumpMat2("mat2_b * mat2_a", mat2.multiply(mat2.create(), mat2B, mat2A)); + +heading("mat2 element access [row, col]"); +appendLine("N/A"); + +heading("mat2 vector multiply"); +dumpVec2("vec2_input", vec2A); +dumpVec2("mat2_a * vec2", vec2.transformMat2(vec2.create(), vec2A, mat2A)); + +heading("mat2 transpose"); +dumpMat2("mat2_a.transpose", mat2.transpose(mat2.create(), mat2A)); + +heading("mat2 inverse"); +dumpMat2("mat2_a.inverse", mat2.invert(mat2.create(), mat2A)); + +heading("mat3 basics"); +dumpMat3("identity", mat3.create()); +dumpMat3("mat3_a", mat3A); +dumpMat3("mat3_b", mat3B); + +heading("mat3 multiply"); +dumpMat3("mat3_a * mat3_b", mat3.multiply(mat3.create(), mat3A, mat3B)); +dumpMat3("mat3_b * mat3_a", mat3.multiply(mat3.create(), mat3B, mat3A)); + +heading("mat3 element access [row, col]"); +appendLine("N/A"); + +heading("mat3 vector multiply"); +dumpVec2("vec2_input", vec2B); +dumpVec3("vec3_input", vec3C); +dumpVec2("mat3_a * vec2", vec2.transformMat3(vec2.create(), vec2B, mat3A)); +dumpVec3("mat3_a * vec3", vec3.transformMat3(vec3.create(), vec3C, mat3A)); + +heading("mat3 transpose"); +dumpMat3("mat3_a.transpose", mat3.transpose(mat3.create(), mat3A)); + +heading("mat3 inverse"); +dumpMat3("mat3_a.inverse", mat3.invert(mat3.create(), mat3A)); + +heading("mat3 constructors"); +{ + const scale2D = mat3.fromScaling(mat3.create(), vec2.fromValues(2.0, 3.0)); + dumpMat3("scale2d", scale2D); + const translate2D = mat3.fromTranslation(mat3.create(), vec2.fromValues(5.0, 10.0)); + dumpMat3("translate2d", translate2D); + dumpScalar("rotate_angle_radians", rotAngle2D); + const rotate2D = mat3.fromRotation(mat3.create(), rotAngle2D); + dumpMat3("rotate2d", rotate2D); + dumpMat3("translate * rotate * scale", mat3.multiply(mat3.create(), translate2D, mat3.multiply(mat3.create(), rotate2D, scale2D))); +} + +heading("matrix constructors and composition"); +dumpMat4("scale", scaleM); +dumpMat4("translate", translateM); +dumpMat4("rotate_x", rotateXM); +dumpMat4("rotate_y", rotateYM); +dumpMat4("rotate_z", rotateZM); +dumpMat4("pure_rotation = rotate_z * rotate_y * rotate_x", pureRotationM); +dumpMat4("transform = translate * rotate_z * rotate_y * rotate_x * scale", transformM); + +heading("matrix vector multiply"); +dumpVec3("vec3_input", vecA); +dumpVec4("vec4_input", vecB); +dumpVec3("transform * vec3", transformVec3(transformM, vecA)); +dumpVec4("transform * vec4", transformVec4(transformM, vecB)); +dumpVec3("rotate_z * vec3", transformVec3(rotateZM, vecA)); +dumpVec3("translate * vec3", transformVec3(translateM, vecA)); + +heading("quaternion constructors"); +dumpQuat("quat_identity", quat.create()); +dumpQuat("quat_rotate_x", quatX); +dumpQuat("quat_rotate_y", quatY); +dumpQuat("quat_rotate_z", quatZ); +dumpVec3("axis_normalized", axis); +dumpScalar("axis_angle_radians", axisAngle); +dumpQuat("from_axis_angle", axisQuat); +dumpMat4("from_axis_angle.mat4", axisMat); + +heading("quaternion multiply"); +dumpQuat("quat_x", quatX); +dumpQuat("quat_y", quatY); +dumpQuat("quat_z", quatZ); +dumpQuat("quat_multiply(quat_x, quat_y)", quatXY); +dumpQuat("quat_multiply(quat_multiply(quat_x, quat_y), quat_z)", quatXYZ); +dumpMat4("quat_xy.mat4", mat4FromQuat(quatXY)); +dumpMat4("quat_xyz.mat4", mat4FromQuat(quatXYZ)); + +heading("quaternion vector rotate"); +dumpVec3("input", vecA); +dumpVec3("quat_rotate(quat_x, input)", quatRotateVec3(quatX, vecA)); +dumpVec3("quat_rotate(quat_y, input)", quatRotateVec3(quatY, vecA)); +dumpVec3("quat_rotate(quat_z, input)", quatRotateVec3(quatZ, vecA)); +dumpVec3("quat_rotate(from_axis_angle, input)", quatRotateVec3(axisQuat, vecA)); +dumpVec3("quat_z * input", quatRotateVec3(quatZ, vecA)); + +heading("matrix quaternion roundtrip"); +dumpQuat("pure_rotation.quat", pureRotationQuat); +dumpMat4("pure_rotation", pureRotationM); +dumpMat4("pure_rotation.quat.mat4", mat4FromQuat(pureRotationQuat)); +dumpMat4("transform.rotation_only", rotationOnlyM); +dumpQuat("transform.rotation_only.quat", quatFromMat(rotationOnlyM)); +dumpQuat("axis_mat.quat", quatFromMat(axisMat)); +dumpMat4("axis_mat.quat.mat4", mat4FromQuat(quatFromMat(axisMat))); +appendLine("hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero"); +dumpQuat("hard_decomp.quat_original", hardQuat); +dumpQuat("hard_decomp.quat_from_mat", quatFromMat(hardMat)); +dumpMat4("hard_decomp.mat4", hardMat); +dumpMat4("hard_decomp.quat_from_mat.mat4", mat4FromQuat(quatFromMat(hardMat))); + +heading("lookAt matrix"); +appendLine("notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0)"); +dumpMat4("lookAt", mat4.lookAt( + mat4.create(), + vec3.fromValues(5, 5, 5), + vec3.fromValues(0, 0, 0), + vec3.fromValues(0, 1, 0) +)); + +heading("euler angle decomposition"); +appendLine("notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians"); +dumpVec3("pure_rotation.quat.euler", quatToEulerXYZ(pureRotationQuat)); +dumpVec3("from_axis_angle.euler", quatToEulerXYZ(axisQuat)); + +heading("matrix inverse"); +dumpMat4("transform.inverse", mat4.invert(mat4.create(), transformM)); +dumpMat4("pure_rotation.inverse", mat4.invert(mat4.create(), pureRotationM)); + +heading("cross product"); +appendLine("notes: cross(a, b) where a and b are vec3"); +const crossA = vec3.fromValues(1, 0, 0); +const crossB = vec3.fromValues(0, 1, 0); +const crossC = vec3.normalize(vec3.create(), vec3.fromValues(1, 2, 3)); +const crossD = vec3.normalize(vec3.create(), vec3.fromValues(-1, 0.5, 2)); +dumpVec3("cross(x_axis, y_axis)", vec3.cross(vec3.create(), crossA, crossB)); +dumpVec3("cross(y_axis, x_axis)", vec3.cross(vec3.create(), crossB, crossA)); +dumpVec3("cross(c, d)", vec3.cross(vec3.create(), crossC, crossD)); + +heading("slerp"); +appendLine("notes: slerp(a, b, t) between quat_x and quat_z"); +dumpQuat("slerp(quat_x, quat_z, 0.25)", quat.slerp(quat.create(), quatX, quatZ, 0.25)); +dumpQuat("slerp(quat_x, quat_z, 0.5)", quat.slerp(quat.create(), quatX, quatZ, 0.5)); +dumpQuat("slerp(quat_x, quat_z, 0.75)", quat.slerp(quat.create(), quatX, quatZ, 0.75)); + +heading("fromTwoVectors"); +appendLine("notes: quaternion that rotates vector a to vector b"); +const fromA = vec3.fromValues(1, 0, 0); +const fromB = vec3.fromValues(0, 1, 0); +const fromC = vec3.normalize(vec3.create(), vec3.fromValues(1, 2, -1)); +const fromD = vec3.normalize(vec3.create(), vec3.fromValues(-1, 0.5, 2)); +const fromXToY = quat.rotationTo(quat.create(), fromA, fromB); +const fromCToD = quat.rotationTo(quat.create(), fromC, fromD); +dumpQuat("from_x_to_y", fromXToY); +dumpQuat("from_c_to_d", fromCToD); +dumpVec3("verify_x_to_y", vec3.transformQuat(vec3.create(), fromA, fromXToY)); +dumpVec3("verify_c_to_d", vec3.transformQuat(vec3.create(), fromC, fromCToD)); + +heading("quaternion inverse"); +dumpQuat("from_axis_angle.inverse", quat.invert(quat.create(), axisQuat)); +dumpQuat("verify_q_mul_qinv", quat.multiply(quat.create(), axisQuat, quat.invert(quat.create(), axisQuat))); + +heading("quaternion to axis-angle"); +const axisAngle1 = quatToAxisAngle(axisQuat); +const axisAngle2 = quatToAxisAngle(quatXYZ); +dumpVec3("from_axis_angle.axis", axisAngle1.axis); +dumpScalar("from_axis_angle.angle", axisAngle1.angle); +dumpVec3("quat_xyz.axis", axisAngle2.axis); +dumpScalar("quat_xyz.angle", axisAngle2.angle); + +const DEG2RAD_60 = 60 * DEG2RAD; + +heading("perspective matrix"); +appendLine("notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0"); +dumpMat4("perspective", mat4.perspective(mat4.create(), DEG2RAD_60, 1.5, 0.1, 100.0)); + +heading("ortho matrix"); +appendLine("notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0"); +dumpMat4("ortho", mat4.ortho(mat4.create(), -10, 10, -7.5, 7.5, 0.1, 100.0)); + +heading("basis directions"); +dumpVec3("forward", basisForward(matA)); +dumpVec3("right", basisRight(matA)); +dumpVec3("up", basisUp(matA)); + +heading("default matrix printer"); +dumpMat4Default("matrix_a.default", matA); +dumpMat4Default("transform.default", transformM); + +writeFileSync(OutputPath, lines.join("\n") + "\n"); +console.log("Wrote", OutputPath); diff --git a/conformance/dump_glmatrix.txt b/conformance/dump_glmatrix.txt new file mode 100644 index 0000000..e1c1c36 --- /dev/null +++ b/conformance/dump_glmatrix.txt @@ -0,0 +1,423 @@ +== dump == +notes: matrices are printed in common column-major order + +== matrix basics == +identity: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +matrix_a: +[ + +001.000 +005.000 +009.000 +013.000 + +002.000 +006.000 +010.000 +014.000 + +003.000 +007.000 +011.000 +015.000 + +004.000 +008.000 +012.000 +016.000 +] +matrix_b: +[ + -010.000 +050.000 +090.000 +130.000 + -020.000 +060.000 +100.000 +140.000 + -030.000 +070.000 +110.000 +150.000 + -040.000 +080.000 +120.000 +160.000 +] + +== matrix multiply == +matrix_a * matrix_b: +[ + -900.000 +2020.000 +3140.000 +4260.000 + -1000.000 +2280.000 +3560.000 +4840.000 + -1100.000 +2540.000 +3980.000 +5420.000 + -1200.000 +2800.000 +4400.000 +6000.000 +] +matrix_b * matrix_a: +[ + +880.000 +1920.000 +2960.000 +4000.000 + +960.000 +2080.000 +3200.000 +4320.000 + +1040.000 +2240.000 +3440.000 +4640.000 + +1120.000 +2400.000 +3680.000 +4960.000 +] + +== element access [row, col] == +N/A + +== mat2 basics == +identity: +[ + +001.000 +000.000 + +000.000 +001.000 +] +mat2_a: +[ + +001.000 +003.000 + +002.000 +004.000 +] +mat2_b: +[ + +005.000 +007.000 + -006.000 -008.000 +] + +== mat2 multiply == +mat2_a * mat2_b: +[ + -013.000 -017.000 + -014.000 -018.000 +] +mat2_b * mat2_a: +[ + +019.000 +043.000 + -022.000 -050.000 +] + +== mat2 element access [row, col] == +N/A + +== mat2 vector multiply == +vec2_input: <+001.250, -002.500> +mat2_a * vec2: <-006.250, -007.500> + +== mat2 transpose == +mat2_a.transpose: +[ + +001.000 +002.000 + +003.000 +004.000 +] + +== mat2 inverse == +mat2_a.inverse: +[ + -002.000 +001.500 + +001.000 -000.500 +] + +== mat3 basics == +identity: +[ + +001.000 +000.000 +000.000 + +000.000 +001.000 +000.000 + +000.000 +000.000 +001.000 +] +mat3_a: +[ + +001.000 +004.000 +007.000 + +002.000 +005.000 +008.000 + +003.000 +006.000 +010.000 +] +mat3_b: +[ + -001.000 +007.000 +006.000 + +003.000 -002.000 +008.000 + +005.000 +004.000 -003.000 +] + +== mat3 multiply == +mat3_a * mat3_b: +[ + +046.000 +027.000 +017.000 + +053.000 +036.000 +028.000 + +065.000 +049.000 +036.000 +] +mat3_b * mat3_a: +[ + +031.000 +067.000 +109.000 + +023.000 +050.000 +085.000 + +004.000 +022.000 +037.000 +] + +== mat3 element access [row, col] == +N/A + +== mat3 vector multiply == +vec2_input: <+003.000, -001.500> +vec3_input: <+001.000, -002.000, +003.000> +mat3_a * vec2: <+004.000, +006.500> +mat3_a * vec3: <+014.000, +016.000, +021.000> + +== mat3 transpose == +mat3_a.transpose: +[ + +001.000 +002.000 +003.000 + +004.000 +005.000 +006.000 + +007.000 +008.000 +010.000 +] + +== mat3 inverse == +mat3_a.inverse: +[ + -000.667 -000.667 +001.000 + -001.333 +003.667 -002.000 + +001.000 -002.000 +001.000 +] + +== mat3 constructors == +scale2d: +[ + +002.000 +000.000 +000.000 + +000.000 +003.000 +000.000 + +000.000 +000.000 +001.000 +] +translate2d: +[ + +001.000 +000.000 +005.000 + +000.000 +001.000 +010.000 + +000.000 +000.000 +001.000 +] +rotate_angle_radians: +000.785 +rotate2d: +[ + +000.707 -000.707 +000.000 + +000.707 +000.707 +000.000 + +000.000 +000.000 +001.000 +] +translate * rotate * scale: +[ + +001.414 -002.121 +005.000 + +001.414 +002.121 +010.000 + +000.000 +000.000 +001.000 +] + +== matrix constructors and composition == +scale: +[ + +002.000 +000.000 +000.000 +000.000 + +000.000 +003.000 +000.000 +000.000 + +000.000 +000.000 +004.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +translate: +[ + +001.000 +000.000 +000.000 +010.000 + +000.000 +001.000 +000.000 +020.000 + +000.000 +000.000 +001.000 +030.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_x: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +000.799 -000.602 +000.000 + +000.000 +000.602 +000.799 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_y: +[ + +000.921 +000.000 -000.391 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.391 +000.000 +000.921 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_z: +[ + +000.326 -000.946 +000.000 +000.000 + +000.946 +000.326 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation = rotate_z * rotate_y * rotate_x: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform = translate * rotate_z * rotate_y * rotate_x * scale: +[ + +000.599 -002.495 +001.870 +010.000 + +001.741 +000.113 -001.964 +020.000 + +000.781 +001.662 +002.941 +030.000 + +000.000 +000.000 +000.000 +001.000 +] + +== matrix vector multiply == +vec3_input: <+001.250, -002.500, +003.750> +vec4_input: <+001.250, -002.500, +003.750, +001.000> +transform * vec3: <+023.998, +014.529, +037.849> +transform * vec4: <+023.998, +014.529, +037.849, +001.000> +rotate_z * vec3: <+002.771, +000.368, +003.750> +translate * vec3: <+011.250, +017.500, +033.750> + +== quaternion constructors == +quat_identity: <+000.000, +000.000, +000.000, +001.000> +quat_rotate_x: <+000.317, +000.000, +000.000, +000.948> +quat_rotate_y: <+000.000, -000.199, +000.000, +000.980> +quat_rotate_z: <+000.000, +000.000, +000.581, +000.814> +axis_normalized: <+000.267, +000.535, -000.802> +axis_angle_radians: +000.838 +from_axis_angle: <+000.109, +000.217, -000.326, +000.914> +from_axis_angle.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion multiply == +quat_x: <+000.317, +000.000, +000.000, +000.948> +quat_y: <+000.000, -000.199, +000.000, +000.980> +quat_z: <+000.000, +000.000, +000.581, +000.814> +quat_multiply(quat_x, quat_y): <+000.311, -000.189, -000.063, +000.929> +quat_multiply(quat_multiply(quat_x, quat_y), quat_z): <+000.143, -000.334, +000.488, +000.793> +quat_xy.mat4: +[ + +000.921 +000.000 -000.391 +000.000 + -000.235 +000.799 -000.554 +000.000 + +000.312 +000.602 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +quat_xyz.mat4: +[ + +000.300 -000.870 -000.391 +000.000 + +000.679 +000.482 -000.554 +000.000 + +000.671 -000.099 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion vector rotate == +input: <+001.250, -002.500, +003.750> +quat_rotate(quat_x, input): <+001.250, -004.253, +001.490> +quat_rotate(quat_y, input): <-000.315, -002.500, +003.940> +quat_rotate(quat_z, input): <+002.771, +000.368, +003.750> +quat_rotate(from_axis_angle, input): <+000.482, -003.871, +002.580> +quat_z * input: <+002.771, +000.368, +003.750> + +== matrix quaternion roundtrip == +pure_rotation.quat: <+000.363, +000.027, +000.591, +000.720> +pure_rotation: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.quat.mat4: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only: +[ + +000.599 -002.495 +001.870 +000.000 + +001.741 +000.113 -001.964 +000.000 + +000.781 +001.662 +002.941 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only.quat: <+000.840, +000.252, +000.982, +001.079> +axis_mat.quat: <+000.109, +000.217, -000.326, +000.914> +axis_mat.quat.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero +hard_decomp.quat_original: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.quat_from_mat: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.quat_from_mat.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== lookAt matrix == +notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0) +lookAt: +[ + +000.707 +000.000 -000.707 +000.000 + -000.408 +000.816 -000.408 +000.000 + +000.577 +000.577 +000.577 -008.660 + +000.000 +000.000 +000.000 +001.000 +] + +== euler angle decomposition == +notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians +pure_rotation.quat.euler: <+000.646, -000.401, +001.239> +from_axis_angle.euler: <+000.064, +000.487, -000.670> + +== matrix inverse == +transform.inverse: +[ + +000.150 +000.435 +000.195 -016.063 + -000.277 +000.013 +000.185 -003.019 + +000.117 -000.123 +000.184 -004.227 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.inverse: +[ + +000.300 +000.870 +000.391 +000.000 + -000.832 +000.038 +000.554 +000.000 + +000.467 -000.491 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== cross product == +notes: cross(a, b) where a and b are vec3 +cross(x_axis, y_axis): <+000.000, +000.000, +001.000> +cross(y_axis, x_axis): <+000.000, +000.000, -001.000> +cross(c, d): <+000.292, -000.583, +000.292> + +== slerp == +notes: slerp(a, b, t) between quat_x and quat_z +slerp(quat_x, quat_z, 0.25): <+000.247, +000.000, +000.157, +000.956> +slerp(quat_x, quat_z, 0.5): <+000.169, +000.000, +000.308, +000.936> +slerp(quat_x, quat_z, 0.75): <+000.086, +000.000, +000.451, +000.888> + +== fromTwoVectors == +notes: quaternion that rotates vector a to vector b +from_x_to_y: <+000.000, +000.000, +000.707, +000.707> +from_c_to_d: <+000.707, -000.157, +000.393, +000.567> +verify_x_to_y: <+000.000, +001.000, +000.000> +verify_c_to_d: <-000.436, +000.218, +000.873> + +== quaternion inverse == +from_axis_angle.inverse: <-000.109, -000.217, +000.326, +000.914> +verify_q_mul_qinv: <+000.000, +000.000, +000.000, +001.000> + +== quaternion to axis-angle == +from_axis_angle.axis: <+000.267, +000.535, -000.802> +from_axis_angle.angle: +000.838 +quat_xyz.axis: <+000.235, -000.549, +000.802> +quat_xyz.angle: +001.309 + +== perspective matrix == +notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0 +perspective: +[ + +001.155 +000.000 +000.000 +000.000 + +000.000 +001.732 +000.000 +000.000 + +000.000 +000.000 -001.002 -000.200 + +000.000 +000.000 -001.000 +000.000 +] + +== ortho matrix == +notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0 +ortho: +[ + +000.100 +000.000 +000.000 +000.000 + +000.000 +000.133 +000.000 +000.000 + +000.000 +000.000 -000.020 -001.002 + +000.000 +000.000 +000.000 +001.000 +] + +== basis directions == +forward: <+009.000, +010.000, +011.000> +right: <+001.000, +002.000, +003.000> +up: <+005.000, +006.000, +007.000> + +== default matrix printer == +matrix_a.default: mat4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) +transform.default: mat4(0.5993741750717163, 1.740708827972412, 0.7814622521400452, 0, -2.495043992996216, 0.11302084475755692, 1.6619211435317993, 0, 1.8697327375411987, -1.9639301300048828, 2.940591335296631, 0, 10, 20, 30, 1) diff --git a/conformance/dump_glsl.comp b/conformance/dump_glsl.comp new file mode 100644 index 0000000..2bf820c --- /dev/null +++ b/conformance/dump_glsl.comp @@ -0,0 +1,399 @@ +#version 410 +out vec4 fragColor; + +uniform mat4 uIdentityM; +uniform mat4 uMatA; +uniform mat4 uMatB; +uniform mat4 uScaleM; +uniform mat4 uTranslateM; +uniform mat4 uRotateXM; +uniform mat4 uRotateYM; +uniform mat4 uRotateZM; +uniform mat4 uAxisMat; +uniform mat4 uHardMat; +uniform vec3 uVecA; +uniform vec4 uVecB; +uniform mat2 uMat2A; +uniform mat2 uMat2B; +uniform mat3 uMat3A; +uniform mat3 uMat3B; +uniform vec2 uVec2A; +uniform vec2 uVec2B; +uniform vec3 uVec3C; + +const float angleA = radians(37.0); +const float angleB = radians(-23.0); +const float angleC = radians(71.0); +const float axisAngle = radians(48.0); +const float rotAngle2D = radians(45.0); + +mat4 identityM() { + return uIdentityM; +} + +mat4 matA() { + return uMatA; +} + +mat4 matB() { + return uMatB; +} + +vec3 vecA() { + return uVecA; +} + +vec4 vecB() { + return uVecB; +} + +mat2 mat2A() { + return uMat2A; +} + +mat2 mat2B() { + return uMat2B; +} + +mat3 mat3A() { + return uMat3A; +} + +mat3 mat3B() { + return uMat3B; +} + +vec2 vec2A() { + return uVec2A; +} + +vec2 vec2B() { + return uVec2B; +} + +vec3 vec3C() { + return uVec3C; +} + +mat4 scaleM() { + return uScaleM; +} + +mat4 translateM() { + return uTranslateM; +} + +mat4 rotateXM() { + return uRotateXM; +} + +mat4 rotateYM() { + return uRotateYM; +} + +mat4 rotateZM() { + return uRotateZM; +} + +mat4 pureRotationM() { + return rotateZM() * rotateYM() * rotateXM(); +} + +vec3 axisNormalized() { + return normalize(vec3(1.0, 2.0, -3.0)); +} + +vec4 quatIdentity() { + return vec4(0.0, 0.0, 0.0, 1.0); +} + +vec4 quatAxisAngle(vec3 axis, float angle) { + vec3 a = normalize(axis); + float s = sin(angle * 0.5); + return vec4(a * s, cos(angle * 0.5)); +} + +vec4 quatX() { + return quatAxisAngle(vec3(1.0, 0.0, 0.0), angleA); +} + +vec4 quatY() { + return quatAxisAngle(vec3(0.0, 1.0, 0.0), angleB); +} + +vec4 quatZ() { + return quatAxisAngle(vec3(0.0, 0.0, 1.0), angleC); +} + +vec4 axisQuat() { + return quatAxisAngle(axisNormalized(), axisAngle); +} + +vec4 quatMultiply(vec4 a, vec4 b) { + return vec4( + a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y + a.y * b.w + a.z * b.x - a.x * b.z, + a.w * b.z + a.z * b.w + a.x * b.y - a.y * b.x, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z + ); +} + +vec3 quatRotate(vec4 q, vec3 v) { + vec3 uv = cross(q.xyz, v); + vec3 uuv = cross(q.xyz, uv); + return v + ((uv * q.w) + uuv) * 2.0; +} + +mat4 quatMat4(vec4 q) { + float xx = q.x * q.x; + float xy = q.x * q.y; + float xz = q.x * q.z; + float xw = q.x * q.w; + float yy = q.y * q.y; + float yz = q.y * q.z; + float yw = q.y * q.w; + float zz = q.z * q.z; + float zw = q.z * q.w; + + return mat4( + vec4(1.0 - 2.0 * (yy + zz), 2.0 * (xy + zw), 2.0 * (xz - yw), 0.0), + vec4(2.0 * (xy - zw), 1.0 - 2.0 * (xx + zz), 2.0 * (yz + xw), 0.0), + vec4(2.0 * (xz + yw), 2.0 * (yz - xw), 1.0 - 2.0 * (xx + yy), 0.0), + vec4(0.0, 0.0, 0.0, 1.0) + ); +} + +vec4 mat4Quat(mat4 m) { + float m00 = m[0][0]; + float m11 = m[1][1]; + float m22 = m[2][2]; + + float fourXSquaredMinus1 = m00 - m11 - m22; + float fourYSquaredMinus1 = m11 - m00 - m22; + float fourZSquaredMinus1 = m22 - m00 - m11; + float fourWSquaredMinus1 = m00 + m11 + m22; + + int biggestIndex = 0; + float fourBiggestSquaredMinus1 = fourWSquaredMinus1; + if (fourXSquaredMinus1 > fourBiggestSquaredMinus1) { + fourBiggestSquaredMinus1 = fourXSquaredMinus1; + biggestIndex = 1; + } + if (fourYSquaredMinus1 > fourBiggestSquaredMinus1) { + fourBiggestSquaredMinus1 = fourYSquaredMinus1; + biggestIndex = 2; + } + if (fourZSquaredMinus1 > fourBiggestSquaredMinus1) { + fourBiggestSquaredMinus1 = fourZSquaredMinus1; + biggestIndex = 3; + } + + float biggestVal = sqrt(fourBiggestSquaredMinus1 + 1.0) * 0.5; + float mult = 0.25 / biggestVal; + vec4 q = vec4(0.0); + + if (biggestIndex == 0) { + q.w = biggestVal; + q.x = (m[1][2] - m[2][1]) * mult; + q.y = (m[2][0] - m[0][2]) * mult; + q.z = (m[0][1] - m[1][0]) * mult; + } else if (biggestIndex == 1) { + q.w = (m[1][2] - m[2][1]) * mult; + q.x = biggestVal; + q.y = (m[0][1] + m[1][0]) * mult; + q.z = (m[2][0] + m[0][2]) * mult; + } else if (biggestIndex == 2) { + q.w = (m[2][0] - m[0][2]) * mult; + q.x = (m[0][1] + m[1][0]) * mult; + q.y = biggestVal; + q.z = (m[1][2] + m[2][1]) * mult; + } else { + q.w = (m[0][1] - m[1][0]) * mult; + q.x = (m[2][0] + m[0][2]) * mult; + q.y = (m[1][2] + m[2][1]) * mult; + q.z = biggestVal; + } + + return q; +} + +mat4 axisMat() { + return uAxisMat; +} + +mat4 transformM() { + return translateM() * rotateZM() * rotateYM() * rotateXM() * scaleM(); +} + +mat4 rotationOnly(mat4 m) { + m[3] = vec4(0.0, 0.0, 0.0, 1.0); + return m; +} + +vec3 hardAxis() { + return normalize(vec3(1.0, -2.0, 3.0)); +} + +vec4 hardQuat() { + return quatAxisAngle(hardAxis(), radians(170.0)); +} + +mat4 hardMat() { + return uHardMat; +} + +mat4 frustumM(float left, float right, float bottom, float top, float nearV, float farV) { + float rl = right - left; + float tb = top - bottom; + float fn = farV - nearV; + return mat4( + vec4((nearV * 2.0) / rl, 0.0, 0.0, 0.0), + vec4(0.0, (nearV * 2.0) / tb, 0.0, 0.0), + vec4((right + left) / rl, (top + bottom) / tb, -(farV + nearV) / fn, -1.0), + vec4(0.0, 0.0, -(farV * nearV * 2.0) / fn, 0.0) + ); +} + +mat4 perspectiveM(float fovyDeg, float aspect, float nearV, float farV) { + float top = nearV * tan(fovyDeg * 3.14159265359 / 360.0); + float right = top * aspect; + return frustumM(-right, right, -top, top, nearV, farV); +} + +mat4 orthoM(float left, float right, float bottom, float top, float nearV, float farV) { + float rl = right - left; + float tb = top - bottom; + float fn = farV - nearV; + return mat4( + vec4(2.0 / rl, 0.0, 0.0, 0.0), + vec4(0.0, 2.0 / tb, 0.0, 0.0), + vec4(0.0, 0.0, -2.0 / fn, 0.0), + vec4(-(left + right) / rl, -(top + bottom) / tb, -(farV + nearV) / fn, 1.0) + ); +} + +mat4 lookAtM(vec3 eye, vec3 center, vec3 up) { + if (all(equal(eye, center))) { + return identityM(); + } + + vec3 z = normalize(eye - center); + vec3 x = cross(up, z); + if (length(x) == 0.0) { + x = vec3(0.0); + } else { + x = normalize(x); + } + vec3 y = cross(z, x); + + return mat4( + vec4(x.x, y.x, z.x, 0.0), + vec4(x.y, y.y, z.y, 0.0), + vec4(x.z, y.z, z.z, 0.0), + vec4(-dot(x, eye), -dot(y, eye), -dot(z, eye), 1.0) + ); +} + +vec4 quatInverse(vec4 q) { + float d = dot(q, q); + return vec4(-q.x / d, -q.y / d, -q.z / d, q.w / d); +} + +vec3 orthogonalV(vec3 v) { + vec3 av = abs(v); + vec3 other; + if (av.x < av.y) { + other = av.x < av.z ? vec3(1.0, 0.0, 0.0) : vec3(0.0, 0.0, 1.0); + } else { + other = av.y < av.z ? vec3(0.0, 1.0, 0.0) : vec3(0.0, 0.0, 1.0); + } + return cross(av, other); +} + +vec4 fromTwoVectorsQ(vec3 a, vec3 b) { + vec3 u = normalize(b); + vec3 v = normalize(a); + if (all(equal(u, -v))) { + vec3 q = normalize(orthogonalV(u)); + return vec4(q.x, q.y, q.z, 0.0); + } + vec3 halfV = normalize(u + v); + vec3 q = cross(v, halfV); + float w = dot(v, halfV); + return vec4(q.x, q.y, q.z, w); +} + +vec4 slerpQ(vec4 a, vec4 b, float t) { + vec4 z = b; + float cosTheta = dot(a, b); + if (cosTheta < 0.0) { + z = -b; + cosTheta = -cosTheta; + } + if (cosTheta > 1.0 - 1e-6) { + return vec4( + a.x + (z.x - a.x) * t, + a.y + (z.y - a.y) * t, + a.z + (z.z - a.z) * t, + a.w + (z.w - a.w) * t + ); + } else { + float angle = acos(cosTheta); + return (sin((1.0 - t) * angle) * a + sin(t * angle) * z) / sin(angle); + } +} + +vec3 quatAxisOnly(vec4 q) { + float cosAngle = q.w; + float sinAngle = sqrt(max(0.0, 1.0 - cosAngle * cosAngle)); + if (abs(sinAngle) < 0.0005) { + sinAngle = 1.0; + } + return vec3(q.x / sinAngle, q.y / sinAngle, q.z / sinAngle); +} + +float quatAngleOnly(vec4 q) { + return acos(q.w) * 2.0; +} + +vec3 quatToAngles(vec4 q) { + float x = q.x; + float y = q.y; + float z = q.z; + float w = q.w; + return vec3( + atan(2.0 * (w * x + y * z), 1.0 - 2.0 * (x * x + y * y)), + asin(2.0 * (w * y - z * x)), + atan(2.0 * (w * z + x * y), 1.0 - 2.0 * (y * y + z * z)) + ); +} + +mat3 scale2D(vec2 s) { + return mat3( + vec3(s.x, 0.0, 0.0), + vec3(0.0, s.y, 0.0), + vec3(0.0, 0.0, 1.0) + ); +} + +mat3 translate2D(vec2 t) { + return mat3( + vec3(1.0, 0.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(t.x, t.y, 1.0) + ); +} + +mat3 rotate2D(float angle) { + float c = cos(angle); + float s = sin(angle); + return mat3( + vec3(c, s, 0.0), + vec3(-s, c, 0.0), + vec3(0.0, 0.0, 1.0) + ); +} + +void main() { + __MAIN_BODY__ +} diff --git a/conformance/dump_glsl.nim b/conformance/dump_glsl.nim new file mode 100644 index 0000000..c37fbc5 --- /dev/null +++ b/conformance/dump_glsl.nim @@ -0,0 +1,644 @@ +import + std/[math, os, strformat, strutils], + opengl, + windy, + vmath + +const + OutputPath = parentDir(currentSourcePath()) / "dump_glsl.txt" + ShaderPath = parentDir(currentSourcePath()) / "dump_glsl.comp" + + VertexShaderSrc = """ +#version 410 +void main() { + float x = (gl_VertexID == 1) ? 3.0 : -1.0; + float y = (gl_VertexID == 2) ? 3.0 : -1.0; + gl_Position = vec4(x, y, 0.0, 1.0); +} +""" + +var + contextInitialized = false + window: Window + vao: GLuint + +proc ensureContext() = + if contextInitialized: + return + window = newWindow( + title = "GLSL dump", + size = ivec2(100, 100), + visible = false, + openglVersion = OpenGL4Dot1 + ) + window.makeContextCurrent() + loadExtensions() + glGenVertexArrays(1, vao.addr) + glBindVertexArray(vao) + glDisable(GL_DEPTH_TEST) + glDisable(GL_BLEND) + contextInitialized = true + +proc compileShader(shaderType: GLenum, source: string): GLuint = + result = glCreateShader(shaderType) + var srcArray = allocCStringArray([source]) + glShaderSource(result, 1, srcArray, nil) + deallocCStringArray(srcArray) + glCompileShader(result) + var status: GLint + glGetShaderiv(result, GL_COMPILE_STATUS, status.addr) + if status == 0: + var logLen: GLint + glGetShaderiv(result, GL_INFO_LOG_LENGTH, logLen.addr) + var log = newString(logLen) + glGetShaderInfoLog(result, logLen, nil, log.cstring) + echo "Shader compile error:" + echo log + echo "Source:" + echo source + quit(1) + +proc compileProgram(vertSrc, fragSrc: string): GLuint = + let vertShader = compileShader(GL_VERTEX_SHADER, vertSrc) + let fragShader = compileShader(GL_FRAGMENT_SHADER, fragSrc) + result = glCreateProgram() + glAttachShader(result, vertShader) + glAttachShader(result, fragShader) + glLinkProgram(result) + var status: GLint + glGetProgramiv(result, GL_LINK_STATUS, status.addr) + if status == 0: + var logLen: GLint + glGetProgramiv(result, GL_INFO_LOG_LENGTH, logLen.addr) + var log = newString(logLen) + glGetProgramInfoLog(result, logLen, nil, log.cstring) + echo "Program link error:" + echo log + quit(1) + glDeleteShader(vertShader) + glDeleteShader(fragShader) + +proc cleanFloat(value: float32): float32 = + if abs(value) < 0.0000005'f32: + 0'f32 + else: + value + +proc fmt(value: float32): string = + let cleaned = cleanFloat(value) + let sign = if cleaned < 0'f32: "-" else: "+" + + var whole = int(floor(abs(cleaned).float64)) + var frac = int(round((abs(cleaned).float64 - whole.float64) * 1000.0)) + if frac == 1000: + inc whole + frac = 0 + + result = sign + let wholeText = $whole + for _ in wholeText.len ..< 3: + result.add '0' + result.add wholeText + result.add '.' + + let fracText = $frac + for _ in fracText.len ..< 3: + result.add '0' + result.add fracText + +proc appendLine(lines: var seq[string], line = "") = + lines.add line + +proc dumpScalar(lines: var seq[string], label: string, value: float32) = + lines.appendLine(label & ": " & fmt(value)) + +proc dumpVec2(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ": <" & fmt(value[0]) & ", " & fmt(value[1]) & ">") + +proc dumpVec3(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ": <" & fmt(value[0]) & ", " & fmt(value[1]) & ", " & fmt(value[2]) & ">") + +proc dumpVec4(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ": <" & fmt(value[0]) & ", " & fmt(value[1]) & ", " & fmt(value[2]) & ", " & fmt(value[3]) & ">") + +proc dumpQuat(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ": <" & fmt(value[0]) & ", " & fmt(value[1]) & ", " & fmt(value[2]) & ", " & fmt(value[3]) & ">") + +proc dumpMat2(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ":") + lines.appendLine("[") + lines.appendLine(" " & fmt(value[0]) & " " & fmt(value[2])) + lines.appendLine(" " & fmt(value[1]) & " " & fmt(value[3])) + lines.appendLine("]") + +proc dumpMat3(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ":") + lines.appendLine("[") + lines.appendLine(" " & fmt(value[0]) & " " & fmt(value[3]) & " " & fmt(value[6])) + lines.appendLine(" " & fmt(value[1]) & " " & fmt(value[4]) & " " & fmt(value[7])) + lines.appendLine(" " & fmt(value[2]) & " " & fmt(value[5]) & " " & fmt(value[8])) + lines.appendLine("]") + +proc dumpMat4(lines: var seq[string], label: string, value: openArray[float32]) = + lines.appendLine(label & ":") + lines.appendLine("[") + lines.appendLine(" " & fmt(value[0]) & " " & fmt(value[4]) & " " & fmt(value[8]) & " " & fmt(value[12])) + lines.appendLine(" " & fmt(value[1]) & " " & fmt(value[5]) & " " & fmt(value[9]) & " " & fmt(value[13])) + lines.appendLine(" " & fmt(value[2]) & " " & fmt(value[6]) & " " & fmt(value[10]) & " " & fmt(value[14])) + lines.appendLine(" " & fmt(value[3]) & " " & fmt(value[7]) & " " & fmt(value[11]) & " " & fmt(value[15])) + lines.appendLine("]") + +proc heading(lines: var seq[string], title: string) = + if lines.len > 0: + lines.appendLine() + lines.appendLine("== " & title & " ==") + +proc shaderTemplate(mainBody: string): string = + readFile(ShaderPath).replace("__MAIN_BODY__", mainBody) + +proc setUniformMat4(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 16 + glUniformMatrix4fv(location, 1, GLboolean(GL_FALSE), cast[ptr GLfloat](unsafeAddr value[0])) + +proc setUniformMat3(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 9 + glUniformMatrix3fv(location, 1, GLboolean(GL_FALSE), cast[ptr GLfloat](unsafeAddr value[0])) + +proc setUniformMat2(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 4 + glUniformMatrix2fv(location, 1, GLboolean(GL_FALSE), cast[ptr GLfloat](unsafeAddr value[0])) + +proc setUniformVec2(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 2 + glUniform2f(location, value[0], value[1]) + +proc setUniformVec3(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 3 + glUniform3f(location, value[0], value[1], value[2]) + +proc setUniformVec4(program: GLuint, name: string, value: openArray[float32]) = + let location = glGetUniformLocation(program, name) + if location < 0: + return + doAssert value.len == 4 + glUniform4f(location, value[0], value[1], value[2], value[3]) + +proc uploadDumpUniforms(program: GLuint) = + let + angleA = degToRad(37.0f) + angleB = degToRad(-23.0f) + angleC = degToRad(71.0f) + axisAngle = degToRad(48.0f) + hardAngle = degToRad(170.0f) + rotateXSin = sin(angleA) + rotateXCos = cos(angleA) + rotateYSin = sin(angleB) + rotateYCos = cos(angleB) + rotateZSin = sin(angleC) + rotateZCos = cos(angleC) + axisLenInv = 1.0f / sqrt(1.0f * 1.0f + 2.0f * 2.0f + (-3.0f) * (-3.0f)) + axisX = 1.0f * axisLenInv + axisY = 2.0f * axisLenInv + axisZ = -3.0f * axisLenInv + axisQuatSin = sin(axisAngle * 0.5f) + axisQuatCos = cos(axisAngle * 0.5f) + axisQuat = [ + axisX * axisQuatSin, + axisY * axisQuatSin, + axisZ * axisQuatSin, + axisQuatCos + ] + hardAxisLenInv = 1.0f / sqrt(1.0f * 1.0f + (-2.0f) * (-2.0f) + 3.0f * 3.0f) + hardAxisX = 1.0f * hardAxisLenInv + hardAxisY = -2.0f * hardAxisLenInv + hardAxisZ = 3.0f * hardAxisLenInv + hardQuatSin = sin(hardAngle * 0.5f) + hardQuatCos = cos(hardAngle * 0.5f) + hardQuat = [ + hardAxisX * hardQuatSin, + hardAxisY * hardQuatSin, + hardAxisZ * hardQuatSin, + hardQuatCos + ] + axisQuatXX = axisQuat[0] * axisQuat[0] + axisQuatXY = axisQuat[0] * axisQuat[1] + axisQuatXZ = axisQuat[0] * axisQuat[2] + axisQuatXW = axisQuat[0] * axisQuat[3] + axisQuatYY = axisQuat[1] * axisQuat[1] + axisQuatYZ = axisQuat[1] * axisQuat[2] + axisQuatYW = axisQuat[1] * axisQuat[3] + axisQuatZZ = axisQuat[2] * axisQuat[2] + axisQuatZW = axisQuat[2] * axisQuat[3] + hardQuatXX = hardQuat[0] * hardQuat[0] + hardQuatXY = hardQuat[0] * hardQuat[1] + hardQuatXZ = hardQuat[0] * hardQuat[2] + hardQuatXW = hardQuat[0] * hardQuat[3] + hardQuatYY = hardQuat[1] * hardQuat[1] + hardQuatYZ = hardQuat[1] * hardQuat[2] + hardQuatYW = hardQuat[1] * hardQuat[3] + hardQuatZZ = hardQuat[2] * hardQuat[2] + hardQuatZW = hardQuat[2] * hardQuat[3] + uIdentityM = [ + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uMatA = [ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + ] + uMatB = [ + -10.0f, -20.0f, -30.0f, -40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, + 90.0f, 100.0f, 110.0f, 120.0f, + 130.0f, 140.0f, 150.0f, 160.0f + ] + uScaleM = [ + 2.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 3.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 4.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uTranslateM = [ + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 10.0f, 20.0f, 30.0f, 1.0f + ] + uRotateXM = [ + 1.0f, 0.0f, 0.0f, 0.0f, + 0.0f, rotateXCos, rotateXSin, 0.0f, + 0.0f, -rotateXSin, rotateXCos, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uRotateYM = [ + rotateYCos, 0.0f, -rotateYSin, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, + rotateYSin, 0.0f, rotateYCos, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uRotateZM = [ + rotateZCos, rotateZSin, 0.0f, 0.0f, + -rotateZSin, rotateZCos, 0.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uAxisMat = [ + 1.0f - 2.0f * (axisQuatYY + axisQuatZZ), 2.0f * (axisQuatXY + axisQuatZW), 2.0f * (axisQuatXZ - axisQuatYW), 0.0f, + 2.0f * (axisQuatXY - axisQuatZW), 1.0f - 2.0f * (axisQuatXX + axisQuatZZ), 2.0f * (axisQuatYZ + axisQuatXW), 0.0f, + 2.0f * (axisQuatXZ + axisQuatYW), 2.0f * (axisQuatYZ - axisQuatXW), 1.0f - 2.0f * (axisQuatXX + axisQuatYY), 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uHardMat = [ + 1.0f - 2.0f * (hardQuatYY + hardQuatZZ), 2.0f * (hardQuatXY + hardQuatZW), 2.0f * (hardQuatXZ - hardQuatYW), 0.0f, + 2.0f * (hardQuatXY - hardQuatZW), 1.0f - 2.0f * (hardQuatXX + hardQuatZZ), 2.0f * (hardQuatYZ + hardQuatXW), 0.0f, + 2.0f * (hardQuatXZ + hardQuatYW), 2.0f * (hardQuatYZ - hardQuatXW), 1.0f - 2.0f * (hardQuatXX + hardQuatYY), 0.0f, + 0.0f, 0.0f, 0.0f, 1.0f + ] + uVecA = [1.25f, -2.5f, 3.75f] + uVecB = [1.25f, -2.5f, 3.75f, 1.0f] + uMat2A = [1.0f, 2.0f, 3.0f, 4.0f] + uMat2B = [5.0f, -6.0f, 7.0f, -8.0f] + uVec2A = [1.25f, -2.5f] + uMat3A = [1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f] + uMat3B = [-1.0f, 3.0f, 5.0f, 7.0f, -2.0f, 4.0f, 6.0f, 8.0f, -3.0f] + uVec2B = [3.0f, -1.5f] + uVec3C = [1.0f, -2.0f, 3.0f] + setUniformMat4(program, "uIdentityM", uIdentityM) + setUniformMat4(program, "uMatA", uMatA) + setUniformMat4(program, "uMatB", uMatB) + setUniformMat4(program, "uScaleM", uScaleM) + setUniformMat4(program, "uTranslateM", uTranslateM) + setUniformMat4(program, "uRotateXM", uRotateXM) + setUniformMat4(program, "uRotateYM", uRotateYM) + setUniformMat4(program, "uRotateZM", uRotateZM) + setUniformMat4(program, "uAxisMat", uAxisMat) + setUniformMat4(program, "uHardMat", uHardMat) + setUniformVec3(program, "uVecA", uVecA) + setUniformVec4(program, "uVecB", uVecB) + setUniformMat2(program, "uMat2A", uMat2A) + setUniformMat2(program, "uMat2B", uMat2B) + setUniformMat3(program, "uMat3A", uMat3A) + setUniformMat3(program, "uMat3B", uMat3B) + setUniformVec2(program, "uVec2A", uVec2A) + setUniformVec2(program, "uVec2B", uVec2B) + setUniformVec3(program, "uVec3C", uVec3C) + +proc runShader(fragSrc: string, pixelCount: int): seq[float32] = + ensureContext() + + let program = compileProgram(VertexShaderSrc, fragSrc) + glUseProgram(program) + uploadDumpUniforms(program) + + var fbo, texture: GLuint + glGenFramebuffers(1, fbo.addr) + glBindFramebuffer(GL_FRAMEBUFFER, fbo) + + glGenTextures(1, texture.addr) + glBindTexture(GL_TEXTURE_2D, texture) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F.GLint, pixelCount.GLsizei, 1.GLsizei, 0.GLint, GL_RGBA, cGL_FLOAT, nil) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.GLint) + glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0) + + let status = glCheckFramebufferStatus(GL_FRAMEBUFFER) + if status != GL_FRAMEBUFFER_COMPLETE: + echo "Framebuffer incomplete: ", status.uint32 + quit(1) + + glViewport(0, 0, pixelCount.GLsizei, 1) + glDrawArrays(GL_TRIANGLES, 0, 3) + + result.setLen(pixelCount * 4) + glReadPixels(0, 0, pixelCount.GLsizei, 1.GLsizei, GL_RGBA, cGL_FLOAT, result[0].addr) + + glBindFramebuffer(GL_FRAMEBUFFER, 0) + glDeleteTextures(1, texture.addr) + glDeleteFramebuffers(1, fbo.addr) + glDeleteProgram(program) + +proc runMatrix(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + int index = int(gl_FragCoord.x); + mat4 value = {expr}; + fragColor = value[index]; +""") + runShader(shaderSrc, 4) + +proc runMat3(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + int index = int(gl_FragCoord.x); + mat3 value = {expr}; + fragColor = vec4(value[index], 0.0); +""") + let raw = runShader(shaderSrc, 3) + result = @[raw[0], raw[1], raw[2], raw[4], raw[5], raw[6], raw[8], raw[9], raw[10]] + +proc runMat2(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + int index = int(gl_FragCoord.x); + mat2 value = {expr}; + fragColor = vec4(value[index], 0.0, 0.0); +""") + let raw = runShader(shaderSrc, 2) + result = @[raw[0], raw[1], raw[4], raw[5]] + +proc runVec3(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + vec3 value = {expr}; + fragColor = vec4(value, 0.0); +""") + runShader(shaderSrc, 1) + +proc runVec4(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + vec4 value = {expr}; + fragColor = value; +""") + runShader(shaderSrc, 1) + +proc runVec2(expr: string): seq[float32] = + let shaderSrc = shaderTemplate(fmt""" + vec2 value = {expr}; + fragColor = vec4(value, 0.0, 0.0); +""") + let raw = runShader(shaderSrc, 1) + result = @[raw[0], raw[1]] + +proc runScalar(expr: string): float32 = + runVec4(expr).toOpenArray(0, 3)[0] + +proc main() = + var lines: seq[string] + + lines.heading("dump") + lines.appendLine("notes: matrices are printed in common column-major order") + + lines.heading("matrix basics") + lines.dumpMat4("identity", runMatrix("identityM()")) + lines.dumpMat4("matrix_a", runMatrix("matA()")) + lines.dumpMat4("matrix_b", runMatrix("matB()")) + + lines.heading("matrix multiply") + lines.dumpMat4("matrix_a * matrix_b", runMatrix("matA() * matB()")) + lines.dumpMat4("matrix_b * matrix_a", runMatrix("matB() * matA()")) + + lines.heading("element access [row, col]") + lines.dumpScalar("transform[0, 0]", runScalar("vec4(matA()[0][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[0, 1]", runScalar("vec4(matA()[0][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[0, 2]", runScalar("vec4(matA()[0][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[0, 3]", runScalar("vec4(matA()[0][3], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[1, 0]", runScalar("vec4(matA()[1][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[1, 1]", runScalar("vec4(matA()[1][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[1, 2]", runScalar("vec4(matA()[1][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[1, 3]", runScalar("vec4(matA()[1][3], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[2, 0]", runScalar("vec4(matA()[2][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[2, 1]", runScalar("vec4(matA()[2][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[2, 2]", runScalar("vec4(matA()[2][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[2, 3]", runScalar("vec4(matA()[2][3], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[3, 0]", runScalar("vec4(matA()[3][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[3, 1]", runScalar("vec4(matA()[3][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[3, 2]", runScalar("vec4(matA()[3][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("transform[3, 3]", runScalar("vec4(matA()[3][3], 0.0, 0.0, 0.0)")) + + lines.heading("mat2 basics") + lines.dumpMat2("identity", runMat2("mat2(1.0)")) + lines.dumpMat2("mat2_a", runMat2("mat2A()")) + lines.dumpMat2("mat2_b", runMat2("mat2B()")) + + lines.heading("mat2 multiply") + lines.dumpMat2("mat2_a * mat2_b", runMat2("mat2A() * mat2B()")) + lines.dumpMat2("mat2_b * mat2_a", runMat2("mat2B() * mat2A()")) + + lines.heading("mat2 element access [row, col]") + lines.dumpScalar("mat2[0, 0]", runScalar("vec4(mat2A()[0][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat2[0, 1]", runScalar("vec4(mat2A()[0][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat2[1, 0]", runScalar("vec4(mat2A()[1][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat2[1, 1]", runScalar("vec4(mat2A()[1][1], 0.0, 0.0, 0.0)")) + + lines.heading("mat2 vector multiply") + lines.dumpVec2("vec2_input", runVec2("vec2A()")) + lines.dumpVec2("mat2_a * vec2", runVec2("mat2A() * vec2A()")) + + lines.heading("mat2 transpose") + lines.dumpMat2("mat2_a.transpose", runMat2("transpose(mat2A())")) + + lines.heading("mat2 inverse") + lines.dumpMat2("mat2_a.inverse", runMat2("inverse(mat2A())")) + + lines.heading("mat3 basics") + lines.dumpMat3("identity", runMat3("mat3(1.0)")) + lines.dumpMat3("mat3_a", runMat3("mat3A()")) + lines.dumpMat3("mat3_b", runMat3("mat3B()")) + + lines.heading("mat3 multiply") + lines.dumpMat3("mat3_a * mat3_b", runMat3("mat3A() * mat3B()")) + lines.dumpMat3("mat3_b * mat3_a", runMat3("mat3B() * mat3A()")) + + lines.heading("mat3 element access [row, col]") + lines.dumpScalar("mat3[0, 0]", runScalar("vec4(mat3A()[0][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[0, 1]", runScalar("vec4(mat3A()[0][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[0, 2]", runScalar("vec4(mat3A()[0][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[1, 0]", runScalar("vec4(mat3A()[1][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[1, 1]", runScalar("vec4(mat3A()[1][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[1, 2]", runScalar("vec4(mat3A()[1][2], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[2, 0]", runScalar("vec4(mat3A()[2][0], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[2, 1]", runScalar("vec4(mat3A()[2][1], 0.0, 0.0, 0.0)")) + lines.dumpScalar("mat3[2, 2]", runScalar("vec4(mat3A()[2][2], 0.0, 0.0, 0.0)")) + + lines.heading("mat3 vector multiply") + lines.dumpVec2("vec2_input", runVec2("vec2B()")) + lines.dumpVec3("vec3_input", runVec3("vec3C()")) + lines.dumpVec2("mat3_a * vec2", runVec2("(mat3A() * vec3(vec2B(), 1.0)).xy")) + lines.dumpVec3("mat3_a * vec3", runVec3("mat3A() * vec3C()")) + + lines.heading("mat3 transpose") + lines.dumpMat3("mat3_a.transpose", runMat3("transpose(mat3A())")) + + lines.heading("mat3 inverse") + lines.dumpMat3("mat3_a.inverse", runMat3("inverse(mat3A())")) + + lines.heading("mat3 constructors") + lines.dumpMat3("scale2d", runMat3("scale2D(vec2(2.0, 3.0))")) + lines.dumpMat3("translate2d", runMat3("translate2D(vec2(5.0, 10.0))")) + lines.dumpScalar("rotate_angle_radians", runScalar("vec4(rotAngle2D, 0.0, 0.0, 0.0)")) + lines.dumpMat3("rotate2d", runMat3("rotate2D(rotAngle2D)")) + lines.dumpMat3("translate * rotate * scale", runMat3("translate2D(vec2(5.0, 10.0)) * rotate2D(rotAngle2D) * scale2D(vec2(2.0, 3.0))")) + + lines.heading("matrix constructors and composition") + lines.dumpMat4("scale", runMatrix("scaleM()")) + lines.dumpMat4("translate", runMatrix("translateM()")) + lines.dumpMat4("rotate_x", runMatrix("rotateXM()")) + lines.dumpMat4("rotate_y", runMatrix("rotateYM()")) + lines.dumpMat4("rotate_z", runMatrix("rotateZM()")) + lines.dumpMat4("pure_rotation = rotate_z * rotate_y * rotate_x", runMatrix("pureRotationM()")) + lines.dumpMat4("transform = translate * rotate_z * rotate_y * rotate_x * scale", runMatrix("transformM()")) + + lines.heading("matrix vector multiply") + lines.dumpVec3("vec3_input", runVec3("vecA()")) + lines.dumpVec4("vec4_input", runVec4("vecB()")) + lines.dumpVec3("transform * vec3", runVec3("(transformM() * vec4(vecA(), 1.0)).xyz")) + lines.dumpVec4("transform * vec4", runVec4("transformM() * vecB()")) + lines.dumpVec3("rotate_z * vec3", runVec3("(rotateZM() * vec4(vecA(), 1.0)).xyz")) + lines.dumpVec3("translate * vec3", runVec3("(translateM() * vec4(vecA(), 1.0)).xyz")) + + lines.heading("quaternion constructors") + lines.dumpQuat("quat_identity", runVec4("quatIdentity()")) + lines.dumpQuat("quat_rotate_x", runVec4("quatX()")) + lines.dumpQuat("quat_rotate_y", runVec4("quatY()")) + lines.dumpQuat("quat_rotate_z", runVec4("quatZ()")) + lines.dumpVec3("axis_normalized", runVec3("axisNormalized()")) + lines.dumpScalar("axis_angle_radians", runScalar("vec4(axisAngle, 0.0, 0.0, 0.0)")) + lines.dumpQuat("from_axis_angle", runVec4("axisQuat()")) + lines.dumpMat4("from_axis_angle.mat4", runMatrix("axisMat()")) + + lines.heading("quaternion multiply") + lines.dumpQuat("quat_x", runVec4("quatX()")) + lines.dumpQuat("quat_y", runVec4("quatY()")) + lines.dumpQuat("quat_z", runVec4("quatZ()")) + lines.dumpQuat("quat_multiply(quat_x, quat_y)", runVec4("quatMultiply(quatX(), quatY())")) + lines.dumpQuat("quat_multiply(quat_multiply(quat_x, quat_y), quat_z)", runVec4("quatMultiply(quatMultiply(quatX(), quatY()), quatZ())")) + lines.dumpMat4("quat_xy.mat4", runMatrix("quatMat4(quatMultiply(quatX(), quatY()))")) + lines.dumpMat4("quat_xyz.mat4", runMatrix("quatMat4(quatMultiply(quatMultiply(quatX(), quatY()), quatZ()))")) + + lines.heading("quaternion vector rotate") + lines.dumpVec3("input", runVec3("vecA()")) + lines.dumpVec3("quat_rotate(quat_x, input)", runVec3("quatRotate(quatX(), vecA())")) + lines.dumpVec3("quat_rotate(quat_y, input)", runVec3("quatRotate(quatY(), vecA())")) + lines.dumpVec3("quat_rotate(quat_z, input)", runVec3("quatRotate(quatZ(), vecA())")) + lines.dumpVec3("quat_rotate(from_axis_angle, input)", runVec3("quatRotate(axisQuat(), vecA())")) + lines.dumpVec3("quat_z * input", runVec3("quatRotate(quatZ(), vecA())")) + + lines.heading("matrix quaternion roundtrip") + lines.dumpQuat("pure_rotation.quat", runVec4("mat4Quat(pureRotationM())")) + lines.dumpMat4("pure_rotation", runMatrix("pureRotationM()")) + lines.dumpMat4("pure_rotation.quat.mat4", runMatrix("quatMat4(mat4Quat(pureRotationM()))")) + lines.dumpMat4("transform.rotation_only", runMatrix("rotationOnly(transformM())")) + lines.dumpQuat("transform.rotation_only.quat", runVec4("mat4Quat(rotationOnly(transformM()))")) + lines.dumpQuat("axis_mat.quat", runVec4("mat4Quat(axisMat())")) + lines.dumpMat4("axis_mat.quat.mat4", runMatrix("quatMat4(mat4Quat(axisMat()))")) + lines.appendLine("hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero") + lines.dumpQuat("hard_decomp.quat_original", runVec4("hardQuat()")) + lines.dumpQuat("hard_decomp.quat_from_mat", runVec4("mat4Quat(hardMat())")) + lines.dumpMat4("hard_decomp.mat4", runMatrix("hardMat()")) + lines.dumpMat4("hard_decomp.quat_from_mat.mat4", runMatrix("quatMat4(mat4Quat(hardMat()))")) + + lines.heading("lookAt matrix") + lines.appendLine("notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0)") + lines.dumpMat4("lookAt", runMatrix("lookAtM(vec3(5.0, 5.0, 5.0), vec3(0.0), vec3(0.0, 1.0, 0.0))")) + + lines.heading("euler angle decomposition") + lines.appendLine("notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians") + lines.dumpVec3("pure_rotation.quat.euler", runVec3("quatToAngles(mat4Quat(pureRotationM()))")) + lines.dumpVec3("from_axis_angle.euler", runVec3("quatToAngles(axisQuat())")) + + lines.heading("matrix inverse") + lines.dumpMat4("transform.inverse", runMatrix("inverse(transformM())")) + lines.dumpMat4("pure_rotation.inverse", runMatrix("inverse(pureRotationM())")) + + lines.heading("cross product") + lines.appendLine("notes: cross(a, b) where a and b are vec3") + lines.dumpVec3("cross(x_axis, y_axis)", runVec3("cross(vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0))")) + lines.dumpVec3("cross(y_axis, x_axis)", runVec3("cross(vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0))")) + lines.dumpVec3("cross(c, d)", runVec3("cross(normalize(vec3(1.0, 2.0, 3.0)), normalize(vec3(-1.0, 0.5, 2.0)))")) + + lines.heading("slerp") + lines.appendLine("notes: slerp(a, b, t) between quat_x and quat_z") + lines.dumpQuat("slerp(quat_x, quat_z, 0.25)", runVec4("slerpQ(quatX(), quatZ(), 0.25)")) + lines.dumpQuat("slerp(quat_x, quat_z, 0.5)", runVec4("slerpQ(quatX(), quatZ(), 0.5)")) + lines.dumpQuat("slerp(quat_x, quat_z, 0.75)", runVec4("slerpQ(quatX(), quatZ(), 0.75)")) + + lines.heading("fromTwoVectors") + lines.appendLine("notes: quaternion that rotates vector a to vector b") + lines.dumpQuat("from_x_to_y", runVec4("fromTwoVectorsQ(normalize(vec3(1.0, 0.0, 0.0)), normalize(vec3(0.0, 1.0, 0.0)))")) + lines.dumpQuat("from_c_to_d", runVec4("fromTwoVectorsQ(normalize(vec3(1.0, 2.0, -1.0)), normalize(vec3(-1.0, 0.5, 2.0)))")) + lines.dumpVec3("verify_x_to_y", runVec3("quatRotate(fromTwoVectorsQ(normalize(vec3(1.0, 0.0, 0.0)), normalize(vec3(0.0, 1.0, 0.0))), normalize(vec3(1.0, 0.0, 0.0)))")) + lines.dumpVec3("verify_c_to_d", runVec3("quatRotate(fromTwoVectorsQ(normalize(vec3(1.0, 2.0, -1.0)), normalize(vec3(-1.0, 0.5, 2.0))), normalize(vec3(1.0, 2.0, -1.0)))")) + + lines.heading("quaternion inverse") + lines.dumpQuat("from_axis_angle.inverse", runVec4("quatInverse(axisQuat())")) + lines.dumpQuat("verify_q_mul_qinv", runVec4("quatMultiply(axisQuat(), quatInverse(axisQuat()))")) + + lines.heading("quaternion to axis-angle") + lines.dumpVec3("from_axis_angle.axis", runVec3("quatAxisOnly(axisQuat())")) + lines.dumpScalar("from_axis_angle.angle", runScalar("vec4(quatAngleOnly(axisQuat()), 0.0, 0.0, 0.0)")) + lines.dumpVec3("quat_xyz.axis", runVec3("quatAxisOnly(quatMultiply(quatMultiply(quatX(), quatY()), quatZ()))")) + lines.dumpScalar("quat_xyz.angle", runScalar("vec4(quatAngleOnly(quatMultiply(quatMultiply(quatX(), quatY()), quatZ())), 0.0, 0.0, 0.0)")) + + lines.heading("perspective matrix") + lines.appendLine("notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0") + lines.dumpMat4("perspective", runMatrix("perspectiveM(60.0, 1.5, 0.1, 100.0)")) + + lines.heading("ortho matrix") + lines.appendLine("notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0") + lines.dumpMat4("ortho", runMatrix("orthoM(-10.0, 10.0, -7.5, 7.5, 0.1, 100.0)")) + + lines.heading("basis directions") + lines.appendLine("N/A") + + lines.heading("default matrix printer") + lines.appendLine("N/A") + + writeFile(OutputPath, lines.join("\n") & "\n") + echo "Wrote ", OutputPath + +main() diff --git a/conformance/dump_glsl.txt b/conformance/dump_glsl.txt new file mode 100644 index 0000000..7080815 --- /dev/null +++ b/conformance/dump_glsl.txt @@ -0,0 +1,446 @@ +== dump == +notes: matrices are printed in common column-major order + +== matrix basics == +identity: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +matrix_a: +[ + +001.000 +005.000 +009.000 +013.000 + +002.000 +006.000 +010.000 +014.000 + +003.000 +007.000 +011.000 +015.000 + +004.000 +008.000 +012.000 +016.000 +] +matrix_b: +[ + -010.000 +050.000 +090.000 +130.000 + -020.000 +060.000 +100.000 +140.000 + -030.000 +070.000 +110.000 +150.000 + -040.000 +080.000 +120.000 +160.000 +] + +== matrix multiply == +matrix_a * matrix_b: +[ + -900.000 +2020.000 +3140.000 +4260.000 + -1000.000 +2280.000 +3560.000 +4840.000 + -1100.000 +2540.000 +3980.000 +5420.000 + -1200.000 +2800.000 +4400.000 +6000.000 +] +matrix_b * matrix_a: +[ + +880.000 +1920.000 +2960.000 +4000.000 + +960.000 +2080.000 +3200.000 +4320.000 + +1040.000 +2240.000 +3440.000 +4640.000 + +1120.000 +2400.000 +3680.000 +4960.000 +] + +== element access [row, col] == +transform[0, 0]: +001.000 +transform[0, 1]: +002.000 +transform[0, 2]: +003.000 +transform[0, 3]: +004.000 +transform[1, 0]: +005.000 +transform[1, 1]: +006.000 +transform[1, 2]: +007.000 +transform[1, 3]: +008.000 +transform[2, 0]: +009.000 +transform[2, 1]: +010.000 +transform[2, 2]: +011.000 +transform[2, 3]: +012.000 +transform[3, 0]: +013.000 +transform[3, 1]: +014.000 +transform[3, 2]: +015.000 +transform[3, 3]: +016.000 + +== mat2 basics == +identity: +[ + +001.000 +000.000 + +000.000 +001.000 +] +mat2_a: +[ + +001.000 +003.000 + +002.000 +004.000 +] +mat2_b: +[ + +005.000 +007.000 + -006.000 -008.000 +] + +== mat2 multiply == +mat2_a * mat2_b: +[ + -013.000 -017.000 + -014.000 -018.000 +] +mat2_b * mat2_a: +[ + +019.000 +043.000 + -022.000 -050.000 +] + +== mat2 element access [row, col] == +mat2[0, 0]: +001.000 +mat2[0, 1]: +002.000 +mat2[1, 0]: +003.000 +mat2[1, 1]: +004.000 + +== mat2 vector multiply == +vec2_input: <+001.250, -002.500> +mat2_a * vec2: <-006.250, -007.500> + +== mat2 transpose == +mat2_a.transpose: +[ + +001.000 +002.000 + +003.000 +004.000 +] + +== mat2 inverse == +mat2_a.inverse: +[ + -002.000 +001.500 + +001.000 -000.500 +] + +== mat3 basics == +identity: +[ + +001.000 +000.000 +000.000 + +000.000 +001.000 +000.000 + +000.000 +000.000 +001.000 +] +mat3_a: +[ + +001.000 +004.000 +007.000 + +002.000 +005.000 +008.000 + +003.000 +006.000 +010.000 +] +mat3_b: +[ + -001.000 +007.000 +006.000 + +003.000 -002.000 +008.000 + +005.000 +004.000 -003.000 +] + +== mat3 multiply == +mat3_a * mat3_b: +[ + +046.000 +027.000 +017.000 + +053.000 +036.000 +028.000 + +065.000 +049.000 +036.000 +] +mat3_b * mat3_a: +[ + +031.000 +067.000 +109.000 + +023.000 +050.000 +085.000 + +004.000 +022.000 +037.000 +] + +== mat3 element access [row, col] == +mat3[0, 0]: +001.000 +mat3[0, 1]: +002.000 +mat3[0, 2]: +003.000 +mat3[1, 0]: +004.000 +mat3[1, 1]: +005.000 +mat3[1, 2]: +006.000 +mat3[2, 0]: +007.000 +mat3[2, 1]: +008.000 +mat3[2, 2]: +010.000 + +== mat3 vector multiply == +vec2_input: <+003.000, -001.500> +vec3_input: <+001.000, -002.000, +003.000> +mat3_a * vec2: <+004.000, +006.500> +mat3_a * vec3: <+014.000, +016.000, +021.000> + +== mat3 transpose == +mat3_a.transpose: +[ + +001.000 +002.000 +003.000 + +004.000 +005.000 +006.000 + +007.000 +008.000 +010.000 +] + +== mat3 inverse == +mat3_a.inverse: +[ + -000.667 -000.667 +001.000 + -001.333 +003.667 -002.000 + +001.000 -002.000 +001.000 +] + +== mat3 constructors == +scale2d: +[ + +002.000 +000.000 +000.000 + +000.000 +003.000 +000.000 + +000.000 +000.000 +001.000 +] +translate2d: +[ + +001.000 +000.000 +005.000 + +000.000 +001.000 +010.000 + +000.000 +000.000 +001.000 +] +rotate_angle_radians: +000.785 +rotate2d: +[ + +000.707 -000.707 +000.000 + +000.707 +000.707 +000.000 + +000.000 +000.000 +001.000 +] +translate * rotate * scale: +[ + +001.414 -002.121 +005.000 + +001.414 +002.121 +010.000 + +000.000 +000.000 +001.000 +] + +== matrix constructors and composition == +scale: +[ + +002.000 +000.000 +000.000 +000.000 + +000.000 +003.000 +000.000 +000.000 + +000.000 +000.000 +004.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +translate: +[ + +001.000 +000.000 +000.000 +010.000 + +000.000 +001.000 +000.000 +020.000 + +000.000 +000.000 +001.000 +030.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_x: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +000.799 -000.602 +000.000 + +000.000 +000.602 +000.799 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_y: +[ + +000.921 +000.000 -000.391 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.391 +000.000 +000.921 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_z: +[ + +000.326 -000.946 +000.000 +000.000 + +000.946 +000.326 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation = rotate_z * rotate_y * rotate_x: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform = translate * rotate_z * rotate_y * rotate_x * scale: +[ + +000.599 -002.495 +001.870 +010.000 + +001.741 +000.113 -001.964 +020.000 + +000.781 +001.662 +002.941 +030.000 + +000.000 +000.000 +000.000 +001.000 +] + +== matrix vector multiply == +vec3_input: <+001.250, -002.500, +003.750> +vec4_input: <+001.250, -002.500, +003.750, +001.000> +transform * vec3: <+023.998, +014.529, +037.849> +transform * vec4: <+023.998, +014.529, +037.849, +001.000> +rotate_z * vec3: <+002.771, +000.368, +003.750> +translate * vec3: <+011.250, +017.500, +033.750> + +== quaternion constructors == +quat_identity: <+000.000, +000.000, +000.000, +001.000> +quat_rotate_x: <+000.317, +000.000, +000.000, +000.948> +quat_rotate_y: <+000.000, -000.199, +000.000, +000.980> +quat_rotate_z: <+000.000, +000.000, +000.581, +000.814> +axis_normalized: <+000.267, +000.535, -000.802> +axis_angle_radians: +000.838 +from_axis_angle: <+000.109, +000.217, -000.326, +000.914> +from_axis_angle.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion multiply == +quat_x: <+000.317, +000.000, +000.000, +000.948> +quat_y: <+000.000, -000.199, +000.000, +000.980> +quat_z: <+000.000, +000.000, +000.581, +000.814> +quat_multiply(quat_x, quat_y): <+000.311, -000.189, -000.063, +000.929> +quat_multiply(quat_multiply(quat_x, quat_y), quat_z): <+000.143, -000.334, +000.488, +000.793> +quat_xy.mat4: +[ + +000.921 +000.000 -000.391 +000.000 + -000.235 +000.799 -000.554 +000.000 + +000.312 +000.602 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +quat_xyz.mat4: +[ + +000.300 -000.870 -000.391 +000.000 + +000.679 +000.482 -000.554 +000.000 + +000.671 -000.099 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion vector rotate == +input: <+001.250, -002.500, +003.750> +quat_rotate(quat_x, input): <+001.250, -004.253, +001.490> +quat_rotate(quat_y, input): <-000.315, -002.500, +003.940> +quat_rotate(quat_z, input): <+002.771, +000.368, +003.750> +quat_rotate(from_axis_angle, input): <+000.482, -003.871, +002.580> +quat_z * input: <+002.771, +000.368, +003.750> + +== matrix quaternion roundtrip == +pure_rotation.quat: <+000.363, +000.027, +000.591, +000.720> +pure_rotation: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.quat.mat4: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only: +[ + +000.599 -002.495 +001.870 +000.000 + +001.741 +000.113 -001.964 +000.000 + +000.781 +001.662 +002.941 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only.quat: <+000.840, +000.252, +000.982, +001.079> +axis_mat.quat: <+000.109, +000.217, -000.326, +000.914> +axis_mat.quat.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero +hard_decomp.quat_original: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.quat_from_mat: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.quat_from_mat.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== lookAt matrix == +notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0) +lookAt: +[ + +000.707 +000.000 -000.707 +000.000 + -000.408 +000.816 -000.408 +000.000 + +000.577 +000.577 +000.577 -008.660 + +000.000 +000.000 +000.000 +001.000 +] + +== euler angle decomposition == +notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians +pure_rotation.quat.euler: <+000.646, -000.401, +001.239> +from_axis_angle.euler: <+000.064, +000.487, -000.670> + +== matrix inverse == +transform.inverse: +[ + +000.150 +000.435 +000.195 -016.063 + -000.277 +000.013 +000.185 -003.019 + +000.117 -000.123 +000.184 -004.227 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.inverse: +[ + +000.300 +000.870 +000.391 +000.000 + -000.832 +000.038 +000.554 +000.000 + +000.467 -000.491 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== cross product == +notes: cross(a, b) where a and b are vec3 +cross(x_axis, y_axis): <+000.000, +000.000, +001.000> +cross(y_axis, x_axis): <+000.000, +000.000, -001.000> +cross(c, d): <+000.292, -000.583, +000.292> + +== slerp == +notes: slerp(a, b, t) between quat_x and quat_z +slerp(quat_x, quat_z, 0.25): <+000.247, +000.000, +000.157, +000.956> +slerp(quat_x, quat_z, 0.5): <+000.169, +000.000, +000.308, +000.936> +slerp(quat_x, quat_z, 0.75): <+000.086, +000.000, +000.451, +000.888> + +== fromTwoVectors == +notes: quaternion that rotates vector a to vector b +from_x_to_y: <+000.000, +000.000, +000.707, +000.707> +from_c_to_d: <+000.707, -000.157, +000.393, +000.567> +verify_x_to_y: <+000.000, +001.000, +000.000> +verify_c_to_d: <-000.436, +000.218, +000.873> + +== quaternion inverse == +from_axis_angle.inverse: <-000.109, -000.217, +000.326, +000.914> +verify_q_mul_qinv: <+000.000, +000.000, +000.000, +001.000> + +== quaternion to axis-angle == +from_axis_angle.axis: <+000.267, +000.535, -000.802> +from_axis_angle.angle: +000.838 +quat_xyz.axis: <+000.235, -000.549, +000.802> +quat_xyz.angle: +001.309 + +== perspective matrix == +notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0 +perspective: +[ + +001.155 +000.000 +000.000 +000.000 + +000.000 +001.732 +000.000 +000.000 + +000.000 +000.000 -001.002 -000.200 + +000.000 +000.000 -001.000 +000.000 +] + +== ortho matrix == +notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0 +ortho: +[ + +000.100 +000.000 +000.000 +000.000 + +000.000 +000.133 +000.000 +000.000 + +000.000 +000.000 -000.020 -001.002 + +000.000 +000.000 +000.000 +001.000 +] + +== basis directions == +N/A + +== default matrix printer == +N/A diff --git a/conformance/dump_jolt.cpp b/conformance/dump_jolt.cpp new file mode 100644 index 0000000..711834d --- /dev/null +++ b/conformance/dump_jolt.cpp @@ -0,0 +1,378 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +using namespace JPH; + +static std::string CleanFloat(float value) +{ + if (std::abs(value) < 0.0000005f) + value = 0.0f; + + std::ostringstream out; + out << (value < 0.0f ? "-" : "+") + << std::setw(3) << std::setfill('0') << static_cast(std::floor(std::abs(value))) + << "." + << std::setw(3) << std::setfill('0') << static_cast(std::round((std::abs(value) - std::floor(std::abs(value))) * 1000.0f)); + + std::string result = out.str(); + if (result.size() >= 4 && result.substr(result.size() - 4) == "1000") + { + int whole = std::stoi(result.substr(1, 3)) + 1; + std::ostringstream fixed; + fixed << result[0] + << std::setw(3) << std::setfill('0') << whole + << ".000"; + return fixed.str(); + } + + return result; +} + +static void AppendLine(std::vector &lines, const std::string &line = {}) +{ + lines.push_back(line); +} + +static void DumpScalar(std::vector &lines, const std::string &label, float value) +{ + AppendLine(lines, label + ": " + CleanFloat(value)); +} + +static void DumpVec3(std::vector &lines, const std::string &label, Vec3Arg value) +{ + AppendLine(lines, label + ": <" + CleanFloat(value.GetX()) + ", " + CleanFloat(value.GetY()) + ", " + CleanFloat(value.GetZ()) + ">"); +} + +static void DumpVec4(std::vector &lines, const std::string &label, Vec4Arg value) +{ + AppendLine(lines, label + ": <" + CleanFloat(value.GetX()) + ", " + CleanFloat(value.GetY()) + ", " + CleanFloat(value.GetZ()) + ", " + CleanFloat(value.GetW()) + ">"); +} + +static void DumpQuat(std::vector &lines, const std::string &label, QuatArg value) +{ + AppendLine(lines, label + ": <" + CleanFloat(value.GetX()) + ", " + CleanFloat(value.GetY()) + ", " + CleanFloat(value.GetZ()) + ", " + CleanFloat(value.GetW()) + ">"); +} + +static void DumpMat4(std::vector &lines, const std::string &label, Mat44Arg value) +{ + static_assert(sizeof(Mat44) == sizeof(float) * 16, "Mat44 storage is expected to be 16 floats"); + float d[16]; + std::memcpy(d, &value, sizeof(d)); + + AppendLine(lines, label + ":"); + AppendLine(lines, "["); + AppendLine(lines, " " + CleanFloat(d[0]) + " " + CleanFloat(d[4]) + " " + CleanFloat(d[8]) + " " + CleanFloat(d[12])); + AppendLine(lines, " " + CleanFloat(d[1]) + " " + CleanFloat(d[5]) + " " + CleanFloat(d[9]) + " " + CleanFloat(d[13])); + AppendLine(lines, " " + CleanFloat(d[2]) + " " + CleanFloat(d[6]) + " " + CleanFloat(d[10]) + " " + CleanFloat(d[14])); + AppendLine(lines, " " + CleanFloat(d[3]) + " " + CleanFloat(d[7]) + " " + CleanFloat(d[11]) + " " + CleanFloat(d[15])); + AppendLine(lines, "]"); +} + +static void DumpMat4Default(std::vector &lines, const std::string &label, Mat44Arg value) +{ + std::ostringstream out; + out << value; + AppendLine(lines, label + ": " + out.str()); +} + +static void Heading(std::vector &lines, const std::string &title) +{ + if (!lines.empty()) + AppendLine(lines); + AppendLine(lines, "== " + title + " =="); +} + +static Mat44 RotationOnlyCopy(Mat44 value) +{ + value(0, 3) = 0.0f; + value(1, 3) = 0.0f; + value(2, 3) = 0.0f; + value(3, 3) = 1.0f; + return value; +} + +int main() +{ + // Derive output path from __FILE__ so it works regardless of cwd + std::string source_path(__FILE__); + std::string output_path = source_path.substr(0, source_path.rfind('.')) + ".txt"; + + std::vector lines; + + const float angle_a = 37.0f * JPH_PI / 180.0f; + const float angle_b = -23.0f * JPH_PI / 180.0f; + const float angle_c = 71.0f * JPH_PI / 180.0f; + + const float mat_a_data[16] = { + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + }; + Mat44 mat_a; + std::memcpy(&mat_a, mat_a_data, sizeof(mat_a)); + + const float mat_b_data[16] = { + -10.0f, -20.0f, -30.0f, -40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, + 90.0f, 100.0f, 110.0f, 120.0f, + 130.0f, 140.0f, 150.0f, 160.0f + }; + Mat44 mat_b; + std::memcpy(&mat_b, mat_b_data, sizeof(mat_b)); + + const Vec3 vec_a(1.25f, -2.5f, 3.75f); + const Vec4 vec_b(1.25f, -2.5f, 3.75f, 1.0f); + + const Mat44 scale_m = Mat44::sScale(Vec3(2.0f, 3.0f, 4.0f)); + const Mat44 translate_m = Mat44::sTranslation(Vec3(10.0f, 20.0f, 30.0f)); + const Mat44 rotate_x_m = Mat44::sRotationX(angle_a); + const Mat44 rotate_y_m = Mat44::sRotationY(angle_b); + const Mat44 rotate_z_m = Mat44::sRotationZ(angle_c); + const Mat44 pure_rotation_m = rotate_z_m * rotate_y_m * rotate_x_m; + + const Vec3 axis = Vec3(1.0f, 2.0f, -3.0f).Normalized(); + const float axis_angle = 48.0f * JPH_PI / 180.0f; + const Quat axis_quat = Quat::sRotation(axis, axis_angle); + const Mat44 axis_mat = Mat44::sRotation(axis_quat); + const Mat44 transform_m = translate_m * rotate_z_m * rotate_y_m * rotate_x_m * scale_m; + + const Quat quat_x = Quat::sRotation(Vec3(1.0f, 0.0f, 0.0f), angle_a); + const Quat quat_y = Quat::sRotation(Vec3(0.0f, 1.0f, 0.0f), angle_b); + const Quat quat_z = Quat::sRotation(Vec3(0.0f, 0.0f, 1.0f), angle_c); + const Quat quat_xy = quat_x * quat_y; + const Quat quat_xyz = quat_xy * quat_z; + + const Vec3 hard_axis = Vec3(1.0f, -2.0f, 3.0f).Normalized(); + const float hard_angle = 170.0f * JPH_PI / 180.0f; + const Quat hard_quat = Quat::sRotation(hard_axis, hard_angle); + const Mat44 hard_mat = Mat44::sRotation(hard_quat); + + const Mat44 rotation_only_m = RotationOnlyCopy(transform_m); + + Heading(lines, "dump"); + AppendLine(lines, "notes: matrices are printed in common column-major order"); + + Heading(lines, "matrix basics"); + DumpMat4(lines, "identity", Mat44::sIdentity()); + DumpMat4(lines, "matrix_a", mat_a); + DumpMat4(lines, "matrix_b", mat_b); + + Heading(lines, "matrix multiply"); + DumpMat4(lines, "matrix_a * matrix_b", mat_a * mat_b); + DumpMat4(lines, "matrix_b * matrix_a", mat_b * mat_a); + + Heading(lines, "element access [row, col]"); + + DumpScalar(lines, "transform[0, 0]", mat_a(0, 0)); + DumpScalar(lines, "transform[0, 1]", mat_a(0, 1)); + DumpScalar(lines, "transform[0, 2]", mat_a(0, 2)); + DumpScalar(lines, "transform[0, 3]", mat_a(0, 3)); + DumpScalar(lines, "transform[1, 0]", mat_a(1, 0)); + DumpScalar(lines, "transform[1, 1]", mat_a(1, 1)); + DumpScalar(lines, "transform[1, 2]", mat_a(1, 2)); + DumpScalar(lines, "transform[1, 3]", mat_a(1, 3)); + DumpScalar(lines, "transform[2, 0]", mat_a(2, 0)); + DumpScalar(lines, "transform[2, 1]", mat_a(2, 1)); + DumpScalar(lines, "transform[2, 2]", mat_a(2, 2)); + DumpScalar(lines, "transform[2, 3]", mat_a(2, 3)); + DumpScalar(lines, "transform[3, 0]", mat_a(3, 0)); + DumpScalar(lines, "transform[3, 1]", mat_a(3, 1)); + DumpScalar(lines, "transform[3, 2]", mat_a(3, 2)); + DumpScalar(lines, "transform[3, 3]", mat_a(3, 3)); + + Heading(lines, "mat2 basics"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat2 multiply"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat2 element access [row, col]"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat2 vector multiply"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat2 transpose"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat2 inverse"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 basics"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 multiply"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 element access [row, col]"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 vector multiply"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 transpose"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 inverse"); + AppendLine(lines, "N/A"); + + Heading(lines, "mat3 constructors"); + AppendLine(lines, "N/A"); + + Heading(lines, "matrix constructors and composition"); + DumpMat4(lines, "scale", scale_m); + DumpMat4(lines, "translate", translate_m); + DumpMat4(lines, "rotate_x", rotate_x_m); + DumpMat4(lines, "rotate_y", rotate_y_m); + DumpMat4(lines, "rotate_z", rotate_z_m); + DumpMat4(lines, "pure_rotation = rotate_z * rotate_y * rotate_x", pure_rotation_m); + DumpMat4(lines, "transform = translate * rotate_z * rotate_y * rotate_x * scale", transform_m); + + Heading(lines, "matrix vector multiply"); + DumpVec3(lines, "vec3_input", vec_a); + DumpVec4(lines, "vec4_input", vec_b); + DumpVec3(lines, "transform * vec3", transform_m * vec_a); + DumpVec4(lines, "transform * vec4", transform_m * vec_b); + DumpVec3(lines, "rotate_z * vec3", rotate_z_m * vec_a); + DumpVec3(lines, "translate * vec3", translate_m * vec_a); + + Heading(lines, "quaternion constructors"); + DumpQuat(lines, "quat_identity", Quat::sIdentity()); + DumpQuat(lines, "quat_rotate_x", quat_x); + DumpQuat(lines, "quat_rotate_y", quat_y); + DumpQuat(lines, "quat_rotate_z", quat_z); + DumpVec3(lines, "axis_normalized", axis); + DumpScalar(lines, "axis_angle_radians", axis_angle); + DumpQuat(lines, "from_axis_angle", axis_quat); + DumpMat4(lines, "from_axis_angle.mat4", axis_mat); + + Heading(lines, "quaternion multiply"); + DumpQuat(lines, "quat_x", quat_x); + DumpQuat(lines, "quat_y", quat_y); + DumpQuat(lines, "quat_z", quat_z); + DumpQuat(lines, "quat_multiply(quat_x, quat_y)", quat_xy); + DumpQuat(lines, "quat_multiply(quat_multiply(quat_x, quat_y), quat_z)", quat_xyz); + DumpMat4(lines, "quat_xy.mat4", Mat44::sRotation(quat_xy)); + DumpMat4(lines, "quat_xyz.mat4", Mat44::sRotation(quat_xyz)); + + Heading(lines, "quaternion vector rotate"); + DumpVec3(lines, "input", vec_a); + DumpVec3(lines, "quat_rotate(quat_x, input)", quat_x * vec_a); + DumpVec3(lines, "quat_rotate(quat_y, input)", quat_y * vec_a); + DumpVec3(lines, "quat_rotate(quat_z, input)", quat_z * vec_a); + DumpVec3(lines, "quat_rotate(from_axis_angle, input)", axis_quat * vec_a); + DumpVec3(lines, "quat_z * input", quat_z * vec_a); + + Heading(lines, "matrix quaternion roundtrip"); + const Quat pure_rotation_quat = pure_rotation_m.GetQuaternion(); + DumpQuat(lines, "pure_rotation.quat", pure_rotation_quat); + DumpMat4(lines, "pure_rotation", pure_rotation_m); + DumpMat4(lines, "pure_rotation.quat.mat4", Mat44::sRotation(pure_rotation_quat)); + DumpMat4(lines, "transform.rotation_only", rotation_only_m); + DumpQuat(lines, "transform.rotation_only.quat", rotation_only_m.GetQuaternion()); + const Quat axis_mat_quat = axis_mat.GetQuaternion(); + DumpQuat(lines, "axis_mat.quat", axis_mat_quat); + DumpMat4(lines, "axis_mat.quat.mat4", Mat44::sRotation(axis_mat_quat)); + AppendLine(lines, "hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero"); + DumpQuat(lines, "hard_decomp.quat_original", hard_quat); + const Quat hard_mat_quat = hard_mat.GetQuaternion(); + DumpQuat(lines, "hard_decomp.quat_from_mat", hard_mat_quat); + DumpMat4(lines, "hard_decomp.mat4", hard_mat); + DumpMat4(lines, "hard_decomp.quat_from_mat.mat4", Mat44::sRotation(hard_mat_quat)); + + Heading(lines, "lookAt matrix"); + AppendLine(lines, "notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0)"); + DumpMat4(lines, "lookAt", Mat44::sLookAt(Vec3(5.0f, 5.0f, 5.0f), Vec3(0.0f, 0.0f, 0.0f), Vec3(0.0f, 1.0f, 0.0f))); + + Heading(lines, "euler angle decomposition"); + AppendLine(lines, "notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians"); + DumpVec3(lines, "pure_rotation.quat.euler", pure_rotation_quat.GetEulerAngles()); + DumpVec3(lines, "from_axis_angle.euler", axis_quat.GetEulerAngles()); + + Heading(lines, "matrix inverse"); + DumpMat4(lines, "transform.inverse", transform_m.Inversed()); + DumpMat4(lines, "pure_rotation.inverse", pure_rotation_m.Inversed()); + + Heading(lines, "cross product"); + AppendLine(lines, "notes: cross(a, b) where a and b are vec3"); + DumpVec3(lines, "cross(x_axis, y_axis)", Vec3(1.0f, 0.0f, 0.0f).Cross(Vec3(0.0f, 1.0f, 0.0f))); + DumpVec3(lines, "cross(y_axis, x_axis)", Vec3(0.0f, 1.0f, 0.0f).Cross(Vec3(1.0f, 0.0f, 0.0f))); + const Vec3 cross_c = Vec3(1.0f, 2.0f, 3.0f).Normalized(); + const Vec3 cross_d = Vec3(-1.0f, 0.5f, 2.0f).Normalized(); + DumpVec3(lines, "cross(c, d)", cross_c.Cross(cross_d)); + + Heading(lines, "slerp"); + AppendLine(lines, "notes: slerp(a, b, t) between quat_x and quat_z"); + DumpQuat(lines, "slerp(quat_x, quat_z, 0.25)", quat_x.SLERP(quat_z, 0.25f)); + DumpQuat(lines, "slerp(quat_x, quat_z, 0.5)", quat_x.SLERP(quat_z, 0.5f)); + DumpQuat(lines, "slerp(quat_x, quat_z, 0.75)", quat_x.SLERP(quat_z, 0.75f)); + + Heading(lines, "fromTwoVectors"); + AppendLine(lines, "notes: quaternion that rotates vector a to vector b"); + const Vec3 ft_from_a(1.0f, 0.0f, 0.0f); + const Vec3 ft_from_b(0.0f, 1.0f, 0.0f); + const Vec3 ft_from_c = Vec3(1.0f, 2.0f, -1.0f).Normalized(); + const Vec3 ft_from_d = Vec3(-1.0f, 0.5f, 2.0f).Normalized(); + const Quat ft_qab = Quat::sFromTo(ft_from_a, ft_from_b); + const Quat ft_qcd = Quat::sFromTo(ft_from_c, ft_from_d); + DumpQuat(lines, "from_x_to_y", ft_qab); + DumpQuat(lines, "from_c_to_d", ft_qcd); + DumpVec3(lines, "verify_x_to_y", ft_qab * ft_from_a); + DumpVec3(lines, "verify_c_to_d", ft_qcd * ft_from_c); + + Heading(lines, "quaternion inverse"); + const Quat axis_quat_inv = axis_quat.Inversed(); + DumpQuat(lines, "from_axis_angle.inverse", axis_quat_inv); + DumpQuat(lines, "verify_q_mul_qinv", axis_quat * axis_quat_inv); + + Heading(lines, "quaternion to axis-angle"); + Vec3 aa_axis1; + float aa_angle1 = 0.0f; + axis_quat.GetAxisAngle(aa_axis1, aa_angle1); + Vec3 aa_axis2; + float aa_angle2 = 0.0f; + quat_xyz.GetAxisAngle(aa_axis2, aa_angle2); + DumpVec3(lines, "from_axis_angle.axis", aa_axis1); + DumpScalar(lines, "from_axis_angle.angle", aa_angle1); + DumpVec3(lines, "quat_xyz.axis", aa_axis2); + DumpScalar(lines, "quat_xyz.angle", aa_angle2); + + Heading(lines, "perspective matrix"); + AppendLine(lines, "notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0"); + DumpMat4(lines, "perspective", Mat44::sPerspective(60.0f * JPH_PI / 180.0f, 1.5f, 0.1f, 100.0f)); + + Heading(lines, "ortho matrix"); + AppendLine(lines, "N/A"); + + Heading(lines, "basis directions"); + AppendLine(lines, "N/A"); + + Heading(lines, "default matrix printer"); + DumpMat4Default(lines, "matrix_a.default", mat_a); + DumpMat4Default(lines, "transform.default", transform_m); + + std::ofstream out(output_path, std::ios::binary); + for (size_t i = 0; i < lines.size(); ++i) + { + out << lines[i]; + if (i + 1 < lines.size()) + out << '\n'; + } + out << '\n'; + + std::cout << "Wrote " << output_path << "\n"; + return 0; +} diff --git a/conformance/dump_jolt.nim b/conformance/dump_jolt.nim new file mode 100644 index 0000000..cc902fc --- /dev/null +++ b/conformance/dump_jolt.nim @@ -0,0 +1,58 @@ +import std/[os, osproc, strformat, strutils] + +const + ConformanceDir = parentDir(currentSourcePath()) + RepoRoot = ConformanceDir.parentDir().parentDir() + JoltIncludeDir = RepoRoot / "jolty" / "JoltPhysics" + JoltIssueReportingCpp = JoltIncludeDir / "Jolt" / "Core" / "IssueReporting.cpp" + CppSource = ConformanceDir / "dump_jolt.cpp" +when defined(windows): + const ExeName = "dump_jolt.exe" +else: + const ExeName = "dump_jolt" + +const + ExePath = ConformanceDir / ExeName + +proc quoteArg(value: string): string = + if value.contains({' ', '\t', '"'}): + "\"" & value.replace("\"", "\\\"") & "\"" + else: + value + +proc findCompiler(): string = + let envCompiler = getEnv("CXX") + if envCompiler.len > 0: + return envCompiler + + for candidate in ["g++.exe", "clang++.exe", "g++", "clang++", "c++.exe", "c++"]: + if findExe(candidate).len > 0: + return candidate + + raise newException(OSError, "Could not find a C++ compiler. Set CXX or install g++ / clang++.") + +proc runOrQuit(command: string) = + let (output, exitCode) = execCmdEx(command) + if output.len > 0: + stdout.write output + if exitCode != 0: + quit exitCode + +proc main() = + let compiler = findCompiler() + let compileCmd = [ + quoteArg(compiler), + "-std=c++17", + "-DJPH_DOUBLE_PRECISION", + "-I" & quoteArg(JoltIncludeDir), + quoteArg(CppSource), + quoteArg(JoltIssueReportingCpp), + "-o", + quoteArg(ExePath) + ].join(" ") + + echo fmt"Building {CppSource} with {compiler}" + runOrQuit(compileCmd) + runOrQuit(quoteArg(ExePath)) + +main() diff --git a/conformance/dump_jolt.txt b/conformance/dump_jolt.txt new file mode 100644 index 0000000..38944a2 --- /dev/null +++ b/conformance/dump_jolt.txt @@ -0,0 +1,332 @@ +== dump == +notes: matrices are printed in common column-major order + +== matrix basics == +identity: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +matrix_a: +[ + +001.000 +005.000 +009.000 +013.000 + +002.000 +006.000 +010.000 +014.000 + +003.000 +007.000 +011.000 +015.000 + +004.000 +008.000 +012.000 +016.000 +] +matrix_b: +[ + -010.000 +050.000 +090.000 +130.000 + -020.000 +060.000 +100.000 +140.000 + -030.000 +070.000 +110.000 +150.000 + -040.000 +080.000 +120.000 +160.000 +] + +== matrix multiply == +matrix_a * matrix_b: +[ + -900.000 +2020.000 +3140.000 +4260.000 + -1000.000 +2280.000 +3560.000 +4840.000 + -1100.000 +2540.000 +3980.000 +5420.000 + -1200.000 +2800.000 +4400.000 +6000.000 +] +matrix_b * matrix_a: +[ + +880.000 +1920.000 +2960.000 +4000.000 + +960.000 +2080.000 +3200.000 +4320.000 + +1040.000 +2240.000 +3440.000 +4640.000 + +1120.000 +2400.000 +3680.000 +4960.000 +] + +== element access [row, col] == +transform[0, 0]: +001.000 +transform[0, 1]: +005.000 +transform[0, 2]: +009.000 +transform[0, 3]: +013.000 +transform[1, 0]: +002.000 +transform[1, 1]: +006.000 +transform[1, 2]: +010.000 +transform[1, 3]: +014.000 +transform[2, 0]: +003.000 +transform[2, 1]: +007.000 +transform[2, 2]: +011.000 +transform[2, 3]: +015.000 +transform[3, 0]: +004.000 +transform[3, 1]: +008.000 +transform[3, 2]: +012.000 +transform[3, 3]: +016.000 + +== mat2 basics == +N/A + +== mat2 multiply == +N/A + +== mat2 element access [row, col] == +N/A + +== mat2 vector multiply == +N/A + +== mat2 transpose == +N/A + +== mat2 inverse == +N/A + +== mat3 basics == +N/A + +== mat3 multiply == +N/A + +== mat3 element access [row, col] == +N/A + +== mat3 vector multiply == +N/A + +== mat3 transpose == +N/A + +== mat3 inverse == +N/A + +== mat3 constructors == +N/A + +== matrix constructors and composition == +scale: +[ + +002.000 +000.000 +000.000 +000.000 + +000.000 +003.000 +000.000 +000.000 + +000.000 +000.000 +004.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +translate: +[ + +001.000 +000.000 +000.000 +010.000 + +000.000 +001.000 +000.000 +020.000 + +000.000 +000.000 +001.000 +030.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_x: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +000.799 -000.602 +000.000 + +000.000 +000.602 +000.799 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_y: +[ + +000.921 +000.000 -000.391 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.391 +000.000 +000.921 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_z: +[ + +000.326 -000.946 +000.000 +000.000 + +000.946 +000.326 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation = rotate_z * rotate_y * rotate_x: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform = translate * rotate_z * rotate_y * rotate_x * scale: +[ + +000.599 -002.495 +001.870 +010.000 + +001.741 +000.113 -001.964 +020.000 + +000.781 +001.662 +002.941 +030.000 + +000.000 +000.000 +000.000 +001.000 +] + +== matrix vector multiply == +vec3_input: <+001.250, -002.500, +003.750> +vec4_input: <+001.250, -002.500, +003.750, +001.000> +transform * vec3: <+023.998, +014.529, +037.849> +transform * vec4: <+023.998, +014.529, +037.849, +001.000> +rotate_z * vec3: <+002.771, +000.368, +003.750> +translate * vec3: <+011.250, +017.500, +033.750> + +== quaternion constructors == +quat_identity: <+000.000, +000.000, +000.000, +001.000> +quat_rotate_x: <+000.317, +000.000, +000.000, +000.948> +quat_rotate_y: <+000.000, -000.199, +000.000, +000.980> +quat_rotate_z: <+000.000, +000.000, +000.581, +000.814> +axis_normalized: <+000.267, +000.535, -000.802> +axis_angle_radians: +000.838 +from_axis_angle: <+000.109, +000.217, -000.326, +000.914> +from_axis_angle.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion multiply == +quat_x: <+000.317, +000.000, +000.000, +000.948> +quat_y: <+000.000, -000.199, +000.000, +000.980> +quat_z: <+000.000, +000.000, +000.581, +000.814> +quat_multiply(quat_x, quat_y): <+000.311, -000.189, -000.063, +000.929> +quat_multiply(quat_multiply(quat_x, quat_y), quat_z): <+000.143, -000.334, +000.488, +000.793> +quat_xy.mat4: +[ + +000.921 +000.000 -000.391 +000.000 + -000.235 +000.799 -000.554 +000.000 + +000.312 +000.602 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +quat_xyz.mat4: +[ + +000.300 -000.870 -000.391 +000.000 + +000.679 +000.482 -000.554 +000.000 + +000.671 -000.099 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion vector rotate == +input: <+001.250, -002.500, +003.750> +quat_rotate(quat_x, input): <+001.250, -004.253, +001.490> +quat_rotate(quat_y, input): <-000.315, -002.500, +003.940> +quat_rotate(quat_z, input): <+002.771, +000.368, +003.750> +quat_rotate(from_axis_angle, input): <+000.482, -003.871, +002.580> +quat_z * input: <+002.771, +000.368, +003.750> + +== matrix quaternion roundtrip == +pure_rotation.quat: <+000.363, +000.027, +000.591, +000.720> +pure_rotation: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.quat.mat4: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only: +[ + +000.599 -002.495 +001.870 +000.000 + +001.741 +000.113 -001.964 +000.000 + +000.781 +001.662 +002.941 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only.quat: <+000.840, +000.252, +000.982, +001.079> +axis_mat.quat: <+000.109, +000.217, -000.326, +000.914> +axis_mat.quat.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero +hard_decomp.quat_original: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.quat_from_mat: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.quat_from_mat.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== lookAt matrix == +notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0) +lookAt: +[ + +000.707 +000.000 -000.707 +000.000 + -000.408 +000.816 -000.408 +000.000 + +000.577 +000.577 +000.577 -008.660 + +000.000 +000.000 +000.000 +001.000 +] + +== euler angle decomposition == +notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians +pure_rotation.quat.euler: <+000.646, -000.401, +001.239> +from_axis_angle.euler: <+000.064, +000.487, -000.670> + +== matrix inverse == +transform.inverse: +[ + +000.150 +000.435 +000.195 -016.063 + -000.277 +000.013 +000.185 -003.019 + +000.117 -000.123 +000.184 -004.227 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.inverse: +[ + +000.300 +000.870 +000.391 +000.000 + -000.832 +000.038 +000.554 +000.000 + +000.467 -000.491 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== cross product == +notes: cross(a, b) where a and b are vec3 +cross(x_axis, y_axis): <+000.000, +000.000, +001.000> +cross(y_axis, x_axis): <+000.000, +000.000, -001.000> +cross(c, d): <+000.292, -000.583, +000.292> + +== slerp == +notes: slerp(a, b, t) between quat_x and quat_z +slerp(quat_x, quat_z, 0.25): <+000.247, +000.000, +000.157, +000.956> +slerp(quat_x, quat_z, 0.5): <+000.169, +000.000, +000.308, +000.936> +slerp(quat_x, quat_z, 0.75): <+000.086, +000.000, +000.451, +000.888> + +== fromTwoVectors == +notes: quaternion that rotates vector a to vector b +from_x_to_y: <+000.000, +000.000, +000.707, +000.707> +from_c_to_d: <+000.707, -000.157, +000.393, +000.567> +verify_x_to_y: <+000.000, +001.000, +000.000> +verify_c_to_d: <-000.436, +000.218, +000.873> + +== quaternion inverse == +from_axis_angle.inverse: <-000.109, -000.217, +000.326, +000.914> +verify_q_mul_qinv: <+000.000, +000.000, +000.000, +001.000> + +== quaternion to axis-angle == +from_axis_angle.axis: <+000.267, +000.535, -000.802> +from_axis_angle.angle: +000.838 +quat_xyz.axis: <+000.235, -000.549, +000.802> +quat_xyz.angle: +001.309 + +== perspective matrix == +notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0 +perspective: +[ + +001.155 +000.000 +000.000 +000.000 + +000.000 +001.732 +000.000 +000.000 + +000.000 +000.000 -001.001 -000.100 + +000.000 +000.000 -001.000 +000.000 +] + +== ortho matrix == +N/A + +== basis directions == +N/A + +== default matrix printer == +matrix_a.default: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 +transform.default: 0.599374, 1.74071, 0.781462, 0, -2.49504, 0.113021, 1.66192, 0, 1.86973, -1.96393, 2.94059, 0, 10, 20, 30, 1 diff --git a/conformance/dump_vmath.nim b/conformance/dump_vmath.nim new file mode 100644 index 0000000..052983e --- /dev/null +++ b/conformance/dump_vmath.nim @@ -0,0 +1,389 @@ +import + std/[math, os, strutils], + ../src/vmath {.all.} + +const + OutputPath = parentDir(currentSourcePath()) / "dump_vmath.txt" + +proc cleanFloat(value: float32): float32 = + if abs(value) < 0.0000005'f32: + 0'f32 + else: + value + +proc fmt(value: float32): string = + let cleaned = cleanFloat(value) + let sign = if cleaned < 0'f32: "-" else: "+" + + var whole = int(floor(abs(cleaned).float64)) + var frac = int(round((abs(cleaned).float64 - whole.float64) * 1000.0)) + if frac == 1000: + inc whole + frac = 0 + + result = sign + let wholeText = $whole + for _ in wholeText.len ..< 3: + result.add '0' + result.add wholeText + result.add '.' + + let fracText = $frac + for _ in fracText.len ..< 3: + result.add '0' + result.add fracText + +proc appendLine(lines: var seq[string], line = "") = + lines.add(line) + +proc dumpScalar(lines: var seq[string], label: string, value: float32) = + lines.appendLine(label & ": " & fmt(value)) + +proc dumpVec2(lines: var seq[string], label: string, value: Vec2) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ">") + +proc dumpVec3(lines: var seq[string], label: string, value: Vec3) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ">") + +proc dumpVec4(lines: var seq[string], label: string, value: Vec4) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ", " & fmt(value.w) & ">") + +proc dumpQuat(lines: var seq[string], label: string, value: Quat) = + lines.appendLine(label & ": <" & fmt(value.x) & ", " & fmt(value.y) & ", " & fmt(value.z) & ", " & fmt(value.w) & ">") + +proc dumpMat2(lines: var seq[string], label: string, value: Mat2) = + lines.appendLine(label & ":") + let d = cast[array[4, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[2])) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[3])) + lines.appendLine("]") + +proc dumpMat3(lines: var seq[string], label: string, value: Mat3) = + lines.appendLine(label & ":") + let d = cast[array[9, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[3]) & " " & fmt(d[6])) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[4]) & " " & fmt(d[7])) + lines.appendLine(" " & fmt(d[2]) & " " & fmt(d[5]) & " " & fmt(d[8])) + lines.appendLine("]") + +proc dumpMat4(lines: var seq[string], label: string, value: Mat4) = + lines.appendLine(label & ":") + let d = cast[array[16, float32]](value) + lines.appendLine("[") + lines.appendLine(" " & fmt(d[0]) & " " & fmt(d[4]) & " " & fmt(d[8]) & " " & fmt(d[12]) ) + lines.appendLine(" " & fmt(d[1]) & " " & fmt(d[5]) & " " & fmt(d[9]) & " " & fmt(d[13]) ) + lines.appendLine(" " & fmt(d[2]) & " " & fmt(d[6]) & " " & fmt(d[10]) & " " & fmt(d[14]) ) + lines.appendLine(" " & fmt(d[3]) & " " & fmt(d[7]) & " " & fmt(d[11]) & " " & fmt(d[15]) ) + lines.appendLine("]") + +proc dumpMat4Default(lines: var seq[string], label: string, value: Mat4) = + lines.appendLine(label & ": " & $value) + +proc heading(lines: var seq[string], title: string) = + if lines.len > 0: + lines.appendLine() + lines.appendLine("== " & title & " ==") + +proc rotationOnlyCopy(value: Mat4): Mat4 = + result = value + result[3, 0] = 0 + result[3, 1] = 0 + result[3, 2] = 0 + +proc main() = + var lines: seq[string] + + let + angleA = 37'f32.toRadians + angleB = -23'f32.toRadians + angleC = 71'f32.toRadians + + matA = cast[Mat4]([ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + ]) + + matB = cast[Mat4]([ + -10.0f, -20.0f, -30.0f, -40.0f, + 50.0f, 60.0f, 70.0f, 80.0f, + 90.0f, 100.0f, 110.0f, 120.0f, + 130.0f, 140.0f, 150.0f, 160.0f + ]) + + vecA = vec3(1.25, -2.5, 3.75) + vecB = vec4(1.25, -2.5, 3.75, 1.0) + + scaleM = scale(vec3(2.0, 3.0, 4.0)) + translateM = translate(vec3(10.0, 20.0, 30.0)) + rotateXM = rotateX(angleA) + rotateYM = rotateY(angleB) + rotateZM = rotateZ(angleC) + pureRotationM = rotateZM * rotateYM * rotateXM + axis = normalize(vec3(1.0, 2.0, -3.0)) + axisAngle = 48'f32.toRadians + axisQuat = fromAxisAngle(axis, axisAngle) + axisMat = axisQuat.mat4() + transformM = translateM * rotateZM * rotateYM * rotateXM * scaleM + + quatX = quatRotateX(angleA) + quatY = quatRotateY(angleB) + quatZ = quatRotateZ(angleC) + quatXY = quatMultiply(quatX, quatY) + quatXYZ = quatMultiply(quatXY, quatZ) + + # Hard quat decomposition case: 170° around arbitrary axis (w near zero) + hardAxis = normalize(vec3(1.0, -2.0, 3.0)) + hardAngle = 170'f32.toRadians + hardQuat = fromAxisAngle(hardAxis, hardAngle) + hardMat = hardQuat.mat4() + + mat2A = cast[Mat2]([1.0f, 2.0f, 3.0f, 4.0f]) + mat2B = cast[Mat2]([5.0f, -6.0f, 7.0f, -8.0f]) + vec2A = vec2(1.25, -2.5) + + mat3A = cast[Mat3]([1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f]) + mat3B = cast[Mat3]([-1.0f, 3.0f, 5.0f, 7.0f, -2.0f, 4.0f, 6.0f, 8.0f, -3.0f]) + vec2B = vec2(3.0, -1.5) + vec3C = vec3(1.0, -2.0, 3.0) + + rotAngle2D = 45'f32.toRadians + scale2D = scale(vec2(2.0, 3.0)) + translate2D = translate(vec2(5.0, 10.0)) + rotate2D = rotate(rotAngle2D) + + rotationOnlyM = rotationOnlyCopy(transformM) + basisRight = vec3(1.0, 0.0, 0.0) + basisUp = vec3(0.0, 1.0, 0.0) + basisForward = vec3(0.0, 0.0, 1.0) + + lines.heading("dump") + lines.appendLine("notes: matrices are printed in common column-major order") + + lines.heading("matrix basics") + lines.dumpMat4("identity", mat4()) + lines.dumpMat4("matrix_a", matA) + lines.dumpMat4("matrix_b", matB) + + lines.heading("matrix multiply") + lines.dumpMat4("matrix_a * matrix_b", matA * matB) + lines.dumpMat4("matrix_b * matrix_a", matB * matA) + + lines.heading("element access [row, col]") + lines.dumpScalar("transform[0, 0]", matA[0, 0]) + lines.dumpScalar("transform[0, 1]", matA[0, 1]) + lines.dumpScalar("transform[0, 2]", matA[0, 2]) + lines.dumpScalar("transform[0, 3]", matA[0, 3]) + lines.dumpScalar("transform[1, 0]", matA[1, 0]) + lines.dumpScalar("transform[1, 1]", matA[1, 1]) + lines.dumpScalar("transform[1, 2]", matA[1, 2]) + lines.dumpScalar("transform[1, 3]", matA[1, 3]) + lines.dumpScalar("transform[2, 0]", matA[2, 0]) + lines.dumpScalar("transform[2, 1]", matA[2, 1]) + lines.dumpScalar("transform[2, 2]", matA[2, 2]) + lines.dumpScalar("transform[2, 3]", matA[2, 3]) + lines.dumpScalar("transform[3, 0]", matA[3, 0]) + lines.dumpScalar("transform[3, 1]", matA[3, 1]) + lines.dumpScalar("transform[3, 2]", matA[3, 2]) + lines.dumpScalar("transform[3, 3]", matA[3, 3]) + + lines.heading("mat2 basics") + lines.dumpMat2("identity", mat2()) + lines.dumpMat2("mat2_a", mat2A) + lines.dumpMat2("mat2_b", mat2B) + + lines.heading("mat2 multiply") + lines.dumpMat2("mat2_a * mat2_b", mat2A * mat2B) + lines.dumpMat2("mat2_b * mat2_a", mat2B * mat2A) + + lines.heading("mat2 element access [row, col]") + lines.dumpScalar("mat2[0, 0]", mat2A[0, 0]) + lines.dumpScalar("mat2[0, 1]", mat2A[0, 1]) + lines.dumpScalar("mat2[1, 0]", mat2A[1, 0]) + lines.dumpScalar("mat2[1, 1]", mat2A[1, 1]) + + lines.heading("mat2 vector multiply") + lines.dumpVec2("vec2_input", vec2A) + lines.dumpVec2("mat2_a * vec2", mat2A * vec2A) + + lines.heading("mat2 transpose") + lines.dumpMat2("mat2_a.transpose", transpose(mat2A)) + + lines.heading("mat2 inverse") + lines.dumpMat2("mat2_a.inverse", inverse(mat2A)) + + lines.heading("mat3 basics") + lines.dumpMat3("identity", mat3()) + lines.dumpMat3("mat3_a", mat3A) + lines.dumpMat3("mat3_b", mat3B) + + lines.heading("mat3 multiply") + lines.dumpMat3("mat3_a * mat3_b", mat3A * mat3B) + lines.dumpMat3("mat3_b * mat3_a", mat3B * mat3A) + + lines.heading("mat3 element access [row, col]") + lines.dumpScalar("mat3[0, 0]", mat3A[0, 0]) + lines.dumpScalar("mat3[0, 1]", mat3A[0, 1]) + lines.dumpScalar("mat3[0, 2]", mat3A[0, 2]) + lines.dumpScalar("mat3[1, 0]", mat3A[1, 0]) + lines.dumpScalar("mat3[1, 1]", mat3A[1, 1]) + lines.dumpScalar("mat3[1, 2]", mat3A[1, 2]) + lines.dumpScalar("mat3[2, 0]", mat3A[2, 0]) + lines.dumpScalar("mat3[2, 1]", mat3A[2, 1]) + lines.dumpScalar("mat3[2, 2]", mat3A[2, 2]) + + lines.heading("mat3 vector multiply") + lines.dumpVec2("vec2_input", vec2B) + lines.dumpVec3("vec3_input", vec3C) + lines.dumpVec2("mat3_a * vec2", mat3A * vec2B) + lines.dumpVec3("mat3_a * vec3", mat3A * vec3C) + + lines.heading("mat3 transpose") + lines.dumpMat3("mat3_a.transpose", transpose(mat3A)) + + lines.heading("mat3 inverse") + lines.dumpMat3("mat3_a.inverse", inverse(mat3A)) + + lines.heading("mat3 constructors") + lines.dumpMat3("scale2d", scale2D) + lines.dumpMat3("translate2d", translate2D) + lines.dumpScalar("rotate_angle_radians", rotAngle2D) + lines.dumpMat3("rotate2d", rotate2D) + lines.dumpMat3("translate * rotate * scale", translate2D * rotate2D * scale2D) + + lines.heading("matrix constructors and composition") + lines.dumpMat4("scale", scaleM) + lines.dumpMat4("translate", translateM) + lines.dumpMat4("rotate_x", rotateXM) + lines.dumpMat4("rotate_y", rotateYM) + lines.dumpMat4("rotate_z", rotateZM) + lines.dumpMat4("pure_rotation = rotate_z * rotate_y * rotate_x", pureRotationM) + lines.dumpMat4("transform = translate * rotate_z * rotate_y * rotate_x * scale", transformM) + + lines.heading("matrix vector multiply") + lines.dumpVec3("vec3_input", vecA) + lines.dumpVec4("vec4_input", vecB) + lines.dumpVec3("transform * vec3", transformM * vecA) + lines.dumpVec4("transform * vec4", transformM * vecB) + lines.dumpVec3("rotate_z * vec3", rotateZM * vecA) + lines.dumpVec3("translate * vec3", translateM * vecA) + + lines.heading("quaternion constructors") + lines.dumpQuat("quat_identity", quat()) + lines.dumpQuat("quat_rotate_x", quatX) + lines.dumpQuat("quat_rotate_y", quatY) + lines.dumpQuat("quat_rotate_z", quatZ) + lines.dumpVec3("axis_normalized", axis) + lines.dumpScalar("axis_angle_radians", axisAngle) + lines.dumpQuat("from_axis_angle", axisQuat) + lines.dumpMat4("from_axis_angle.mat4", axisMat) + + lines.heading("quaternion multiply") + lines.dumpQuat("quat_x", quatX) + lines.dumpQuat("quat_y", quatY) + lines.dumpQuat("quat_z", quatZ) + lines.dumpQuat("quat_multiply(quat_x, quat_y)", quatXY) + lines.dumpQuat("quat_multiply(quat_multiply(quat_x, quat_y), quat_z)", quatXYZ) + lines.dumpMat4("quat_xy.mat4", quatXY.mat4()) + lines.dumpMat4("quat_xyz.mat4", quatXYZ.mat4()) + + lines.heading("quaternion vector rotate") + lines.dumpVec3("input", vecA) + lines.dumpVec3("quat_rotate(quat_x, input)", quatRotate(quatX, vecA)) + lines.dumpVec3("quat_rotate(quat_y, input)", quatRotate(quatY, vecA)) + lines.dumpVec3("quat_rotate(quat_z, input)", quatRotate(quatZ, vecA)) + lines.dumpVec3("quat_rotate(from_axis_angle, input)", quatRotate(axisQuat, vecA)) + lines.dumpVec3("quat_z * input", quatZ * vecA) + + lines.heading("matrix quaternion roundtrip") + lines.dumpQuat("pure_rotation.quat", pureRotationM.quat()) + lines.dumpMat4("pure_rotation", pureRotationM) + lines.dumpMat4("pure_rotation.quat.mat4", pureRotationM.quat().mat4()) + lines.dumpMat4("transform.rotation_only", rotationOnlyM) + lines.dumpQuat("transform.rotation_only.quat", rotationOnlyM.quat()) + lines.dumpQuat("axis_mat.quat", axisMat.quat()) + lines.dumpMat4("axis_mat.quat.mat4", axisMat.quat().mat4()) + lines.appendLine("hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero") + lines.dumpQuat("hard_decomp.quat_original", hardQuat) + lines.dumpQuat("hard_decomp.quat_from_mat", hardMat.quat()) + lines.dumpMat4("hard_decomp.mat4", hardMat) + lines.dumpMat4("hard_decomp.quat_from_mat.mat4", hardMat.quat().mat4()) + + lines.heading("lookAt matrix") + lines.appendLine("notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0)") + lines.dumpMat4("lookAt", lookAt(vec3(5'f32, 5'f32, 5'f32), vec3(0'f32, 0'f32, 0'f32), vec3(0'f32, 1'f32, 0'f32))) + + lines.heading("euler angle decomposition") + lines.appendLine("notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians") + let pureRotAngles = pureRotationM.quat().toAngles() + let axisAngles = axisQuat.toAngles() + lines.dumpVec3("pure_rotation.quat.euler", pureRotAngles) + lines.dumpVec3("from_axis_angle.euler", axisAngles) + + lines.heading("matrix inverse") + lines.dumpMat4("transform.inverse", transformM.inverse()) + lines.dumpMat4("pure_rotation.inverse", pureRotationM.inverse()) + + lines.heading("cross product") + lines.appendLine("notes: cross(a, b) where a and b are vec3") + let crossA = vec3(1'f32, 0'f32, 0'f32) + let crossB = vec3(0'f32, 1'f32, 0'f32) + let crossC = normalize(vec3(1'f32, 2'f32, 3'f32)) + let crossD = normalize(vec3(-1'f32, 0.5'f32, 2'f32)) + lines.dumpVec3("cross(x_axis, y_axis)", cross(crossA, crossB)) + lines.dumpVec3("cross(y_axis, x_axis)", cross(crossB, crossA)) + lines.dumpVec3("cross(c, d)", cross(crossC, crossD)) + + lines.heading("slerp") + lines.appendLine("notes: slerp(a, b, t) between quat_x and quat_z") + lines.dumpQuat("slerp(quat_x, quat_z, 0.25)", slerp(quatX, quatZ, 0.25'f32)) + lines.dumpQuat("slerp(quat_x, quat_z, 0.5)", slerp(quatX, quatZ, 0.5'f32)) + lines.dumpQuat("slerp(quat_x, quat_z, 0.75)", slerp(quatX, quatZ, 0.75'f32)) + + lines.heading("fromTwoVectors") + lines.appendLine("notes: quaternion that rotates vector a to vector b") + let fromA = normalize(vec3(1'f32, 0'f32, 0'f32)) + let fromB = normalize(vec3(0'f32, 1'f32, 0'f32)) + let fromC = normalize(vec3(1'f32, 2'f32, -1'f32)) + let fromD = normalize(vec3(-1'f32, 0.5'f32, 2'f32)) + lines.dumpQuat("from_x_to_y", fromTwoVectors(fromA, fromB)) + lines.dumpQuat("from_c_to_d", fromTwoVectors(fromC, fromD)) + lines.dumpVec3("verify_x_to_y", fromTwoVectors(fromA, fromB) * fromA) + lines.dumpVec3("verify_c_to_d", fromTwoVectors(fromC, fromD) * fromC) + + lines.heading("quaternion inverse") + lines.dumpQuat("from_axis_angle.inverse", quatInverse(axisQuat)) + lines.dumpQuat("verify_q_mul_qinv", quatMultiply(axisQuat, quatInverse(axisQuat))) + + lines.heading("quaternion to axis-angle") + let (aaAxis1, aaAngle1) = toAxisAngle(axisQuat) + let (aaAxis2, aaAngle2) = toAxisAngle(quatXYZ) + lines.dumpVec3("from_axis_angle.axis", aaAxis1) + lines.dumpScalar("from_axis_angle.angle", aaAngle1) + lines.dumpVec3("quat_xyz.axis", aaAxis2) + lines.dumpScalar("quat_xyz.angle", aaAngle2) + + lines.heading("perspective matrix") + lines.appendLine("notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0") + lines.dumpMat4("perspective", perspective(60'f32, 1.5'f32, 0.1'f32, 100'f32)) + + lines.heading("ortho matrix") + lines.appendLine("notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0") + lines.dumpMat4("ortho", ortho(-10'f32, 10'f32, -7.5'f32, 7.5'f32, 0.1'f32, 100'f32)) + + lines.heading("basis directions") + lines.dumpVec3("forward", matA.forward()) + lines.dumpVec3("right", matA.right()) + lines.dumpVec3("up", matA.up()) + + lines.heading("default matrix printer") + lines.dumpMat4Default("matrix_a.default", matA) + lines.dumpMat4Default("transform.default", transformM) + + writeFile(OutputPath, lines.join("\n") & "\n") + echo "Wrote ", OutputPath + +main() diff --git a/conformance/dump_vmath.txt b/conformance/dump_vmath.txt new file mode 100644 index 0000000..9988d4f --- /dev/null +++ b/conformance/dump_vmath.txt @@ -0,0 +1,459 @@ +== dump == +notes: matrices are printed in common column-major order + +== matrix basics == +identity: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +matrix_a: +[ + +001.000 +005.000 +009.000 +013.000 + +002.000 +006.000 +010.000 +014.000 + +003.000 +007.000 +011.000 +015.000 + +004.000 +008.000 +012.000 +016.000 +] +matrix_b: +[ + -010.000 +050.000 +090.000 +130.000 + -020.000 +060.000 +100.000 +140.000 + -030.000 +070.000 +110.000 +150.000 + -040.000 +080.000 +120.000 +160.000 +] + +== matrix multiply == +matrix_a * matrix_b: +[ + -900.000 +2020.000 +3140.000 +4260.000 + -1000.000 +2280.000 +3560.000 +4840.000 + -1100.000 +2540.000 +3980.000 +5420.000 + -1200.000 +2800.000 +4400.000 +6000.000 +] +matrix_b * matrix_a: +[ + +880.000 +1920.000 +2960.000 +4000.000 + +960.000 +2080.000 +3200.000 +4320.000 + +1040.000 +2240.000 +3440.000 +4640.000 + +1120.000 +2400.000 +3680.000 +4960.000 +] + +== element access [row, col] == +transform[0, 0]: +001.000 +transform[0, 1]: +002.000 +transform[0, 2]: +003.000 +transform[0, 3]: +004.000 +transform[1, 0]: +005.000 +transform[1, 1]: +006.000 +transform[1, 2]: +007.000 +transform[1, 3]: +008.000 +transform[2, 0]: +009.000 +transform[2, 1]: +010.000 +transform[2, 2]: +011.000 +transform[2, 3]: +012.000 +transform[3, 0]: +013.000 +transform[3, 1]: +014.000 +transform[3, 2]: +015.000 +transform[3, 3]: +016.000 + +== mat2 basics == +identity: +[ + +001.000 +000.000 + +000.000 +001.000 +] +mat2_a: +[ + +001.000 +003.000 + +002.000 +004.000 +] +mat2_b: +[ + +005.000 +007.000 + -006.000 -008.000 +] + +== mat2 multiply == +mat2_a * mat2_b: +[ + -013.000 -017.000 + -014.000 -018.000 +] +mat2_b * mat2_a: +[ + +019.000 +043.000 + -022.000 -050.000 +] + +== mat2 element access [row, col] == +mat2[0, 0]: +001.000 +mat2[0, 1]: +002.000 +mat2[1, 0]: +003.000 +mat2[1, 1]: +004.000 + +== mat2 vector multiply == +vec2_input: <+001.250, -002.500> +mat2_a * vec2: <-006.250, -007.500> + +== mat2 transpose == +mat2_a.transpose: +[ + +001.000 +002.000 + +003.000 +004.000 +] + +== mat2 inverse == +mat2_a.inverse: +[ + -002.000 +001.500 + +001.000 -000.500 +] + +== mat3 basics == +identity: +[ + +001.000 +000.000 +000.000 + +000.000 +001.000 +000.000 + +000.000 +000.000 +001.000 +] +mat3_a: +[ + +001.000 +004.000 +007.000 + +002.000 +005.000 +008.000 + +003.000 +006.000 +010.000 +] +mat3_b: +[ + -001.000 +007.000 +006.000 + +003.000 -002.000 +008.000 + +005.000 +004.000 -003.000 +] + +== mat3 multiply == +mat3_a * mat3_b: +[ + +046.000 +027.000 +017.000 + +053.000 +036.000 +028.000 + +065.000 +049.000 +036.000 +] +mat3_b * mat3_a: +[ + +031.000 +067.000 +109.000 + +023.000 +050.000 +085.000 + +004.000 +022.000 +037.000 +] + +== mat3 element access [row, col] == +mat3[0, 0]: +001.000 +mat3[0, 1]: +002.000 +mat3[0, 2]: +003.000 +mat3[1, 0]: +004.000 +mat3[1, 1]: +005.000 +mat3[1, 2]: +006.000 +mat3[2, 0]: +007.000 +mat3[2, 1]: +008.000 +mat3[2, 2]: +010.000 + +== mat3 vector multiply == +vec2_input: <+003.000, -001.500> +vec3_input: <+001.000, -002.000, +003.000> +mat3_a * vec2: <+004.000, +006.500> +mat3_a * vec3: <+014.000, +016.000, +021.000> + +== mat3 transpose == +mat3_a.transpose: +[ + +001.000 +002.000 +003.000 + +004.000 +005.000 +006.000 + +007.000 +008.000 +010.000 +] + +== mat3 inverse == +mat3_a.inverse: +[ + -000.667 -000.667 +001.000 + -001.333 +003.667 -002.000 + +001.000 -002.000 +001.000 +] + +== mat3 constructors == +scale2d: +[ + +002.000 +000.000 +000.000 + +000.000 +003.000 +000.000 + +000.000 +000.000 +001.000 +] +translate2d: +[ + +001.000 +000.000 +005.000 + +000.000 +001.000 +010.000 + +000.000 +000.000 +001.000 +] +rotate_angle_radians: +000.785 +rotate2d: +[ + +000.707 -000.707 +000.000 + +000.707 +000.707 +000.000 + +000.000 +000.000 +001.000 +] +translate * rotate * scale: +[ + +001.414 -002.121 +005.000 + +001.414 +002.121 +010.000 + +000.000 +000.000 +001.000 +] + +== matrix constructors and composition == +scale: +[ + +002.000 +000.000 +000.000 +000.000 + +000.000 +003.000 +000.000 +000.000 + +000.000 +000.000 +004.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +translate: +[ + +001.000 +000.000 +000.000 +010.000 + +000.000 +001.000 +000.000 +020.000 + +000.000 +000.000 +001.000 +030.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_x: +[ + +001.000 +000.000 +000.000 +000.000 + +000.000 +000.799 -000.602 +000.000 + +000.000 +000.602 +000.799 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_y: +[ + +000.921 +000.000 -000.391 +000.000 + +000.000 +001.000 +000.000 +000.000 + +000.391 +000.000 +000.921 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +rotate_z: +[ + +000.326 -000.946 +000.000 +000.000 + +000.946 +000.326 +000.000 +000.000 + +000.000 +000.000 +001.000 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation = rotate_z * rotate_y * rotate_x: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform = translate * rotate_z * rotate_y * rotate_x * scale: +[ + +000.599 -002.495 +001.870 +010.000 + +001.741 +000.113 -001.964 +020.000 + +000.781 +001.662 +002.941 +030.000 + +000.000 +000.000 +000.000 +001.000 +] + +== matrix vector multiply == +vec3_input: <+001.250, -002.500, +003.750> +vec4_input: <+001.250, -002.500, +003.750, +001.000> +transform * vec3: <+023.998, +014.529, +037.849> +transform * vec4: <+023.998, +014.529, +037.849, +001.000> +rotate_z * vec3: <+002.771, +000.368, +003.750> +translate * vec3: <+011.250, +017.500, +033.750> + +== quaternion constructors == +quat_identity: <+000.000, +000.000, +000.000, +001.000> +quat_rotate_x: <+000.317, +000.000, +000.000, +000.948> +quat_rotate_y: <+000.000, -000.199, +000.000, +000.980> +quat_rotate_z: <+000.000, +000.000, +000.581, +000.814> +axis_normalized: <+000.267, +000.535, -000.802> +axis_angle_radians: +000.838 +from_axis_angle: <+000.109, +000.217, -000.326, +000.914> +from_axis_angle.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion multiply == +quat_x: <+000.317, +000.000, +000.000, +000.948> +quat_y: <+000.000, -000.199, +000.000, +000.980> +quat_z: <+000.000, +000.000, +000.581, +000.814> +quat_multiply(quat_x, quat_y): <+000.311, -000.189, -000.063, +000.929> +quat_multiply(quat_multiply(quat_x, quat_y), quat_z): <+000.143, -000.334, +000.488, +000.793> +quat_xy.mat4: +[ + +000.921 +000.000 -000.391 +000.000 + -000.235 +000.799 -000.554 +000.000 + +000.312 +000.602 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +quat_xyz.mat4: +[ + +000.300 -000.870 -000.391 +000.000 + +000.679 +000.482 -000.554 +000.000 + +000.671 -000.099 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== quaternion vector rotate == +input: <+001.250, -002.500, +003.750> +quat_rotate(quat_x, input): <+001.250, -004.253, +001.490> +quat_rotate(quat_y, input): <-000.315, -002.500, +003.940> +quat_rotate(quat_z, input): <+002.771, +000.368, +003.750> +quat_rotate(from_axis_angle, input): <+000.482, -003.871, +002.580> +quat_z * input: <+002.771, +000.368, +003.750> + +== matrix quaternion roundtrip == +pure_rotation.quat: <+000.363, +000.027, +000.591, +000.720> +pure_rotation: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.quat.mat4: +[ + +000.300 -000.832 +000.467 +000.000 + +000.870 +000.038 -000.491 +000.000 + +000.391 +000.554 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only: +[ + +000.599 -002.495 +001.870 +000.000 + +001.741 +000.113 -001.964 +000.000 + +000.781 +001.662 +002.941 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +transform.rotation_only.quat: <+000.840, +000.252, +000.982, +001.079> +axis_mat.quat: <+000.109, +000.217, -000.326, +000.914> +axis_mat.quat.mat4: +[ + +000.693 +000.643 +000.326 +000.000 + -000.549 +000.764 -000.340 +000.000 + -000.468 +000.057 +000.882 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.note: 170 degrees around (1,-2,3) normalized - w near zero +hard_decomp.quat_original: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.quat_from_mat: <+000.266, -000.532, +000.799, +000.087> +hard_decomp.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] +hard_decomp.quat_from_mat.mat4: +[ + -000.843 -000.423 +000.332 +000.000 + -000.144 -000.418 -000.897 +000.000 + +000.518 -000.804 +000.291 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== lookAt matrix == +notes: eye=(5,5,5), center=(0,0,0), up=(0,1,0) +lookAt: +[ + +000.707 +000.000 -000.707 +000.000 + -000.408 +000.816 -000.408 +000.000 + +000.577 +000.577 +000.577 -008.660 + +000.000 +000.000 +000.000 +001.000 +] + +== euler angle decomposition == +notes: euler angles as vec3(pitch/x, yaw/y, roll/z) in radians +pure_rotation.quat.euler: <+000.646, -000.401, +001.239> +from_axis_angle.euler: <+000.064, +000.487, -000.670> + +== matrix inverse == +transform.inverse: +[ + +000.150 +000.435 +000.195 -016.063 + -000.277 +000.013 +000.185 -003.019 + +000.117 -000.123 +000.184 -004.227 + +000.000 +000.000 +000.000 +001.000 +] +pure_rotation.inverse: +[ + +000.300 +000.870 +000.391 +000.000 + -000.832 +000.038 +000.554 +000.000 + +000.467 -000.491 +000.735 +000.000 + +000.000 +000.000 +000.000 +001.000 +] + +== cross product == +notes: cross(a, b) where a and b are vec3 +cross(x_axis, y_axis): <+000.000, +000.000, +001.000> +cross(y_axis, x_axis): <+000.000, +000.000, -001.000> +cross(c, d): <+000.292, -000.583, +000.292> + +== slerp == +notes: slerp(a, b, t) between quat_x and quat_z +slerp(quat_x, quat_z, 0.25): <+000.247, +000.000, +000.157, +000.956> +slerp(quat_x, quat_z, 0.5): <+000.169, +000.000, +000.308, +000.936> +slerp(quat_x, quat_z, 0.75): <+000.086, +000.000, +000.451, +000.888> + +== fromTwoVectors == +notes: quaternion that rotates vector a to vector b +from_x_to_y: <+000.000, +000.000, +000.707, +000.707> +from_c_to_d: <+000.707, -000.157, +000.393, +000.567> +verify_x_to_y: <+000.000, +001.000, +000.000> +verify_c_to_d: <-000.436, +000.218, +000.873> + +== quaternion inverse == +from_axis_angle.inverse: <-000.109, -000.217, +000.326, +000.914> +verify_q_mul_qinv: <+000.000, +000.000, +000.000, +001.000> + +== quaternion to axis-angle == +from_axis_angle.axis: <+000.267, +000.535, -000.802> +from_axis_angle.angle: +000.838 +quat_xyz.axis: <+000.235, -000.549, +000.802> +quat_xyz.angle: +001.309 + +== perspective matrix == +notes: fovy=60 degrees, aspect=1.5, near=0.1, far=100.0 +perspective: +[ + +001.155 +000.000 +000.000 +000.000 + +000.000 +001.732 +000.000 +000.000 + +000.000 +000.000 -001.002 -000.200 + +000.000 +000.000 -001.000 +000.000 +] + +== ortho matrix == +notes: left=-10, right=10, bottom=-7.5, top=7.5, near=0.1, far=100.0 +ortho: +[ + +000.100 +000.000 +000.000 +000.000 + +000.000 +000.133 +000.000 +000.000 + +000.000 +000.000 -000.020 -001.002 + +000.000 +000.000 +000.000 +001.000 +] + +== basis directions == +forward: <+009.000, +010.000, +011.000> +right: <+001.000, +002.000, +003.000> +up: <+005.000, +006.000, +007.000> + +== default matrix printer == +matrix_a.default: mat4( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 +) +transform.default: mat4( + 0.5993741, 1.7407088, 0.78146225, 0.0, + -2.4950442, 0.11302096, 1.6619208, 0.0, + 1.8697325, -1.9639301, 2.9405916, 0.0, + 10.0, 20.0, 30.0, 1.0 +) diff --git a/src/vmath.nim b/src/vmath.nim index 7537572..e0036e7 100644 --- a/src/vmath.nim +++ b/src/vmath.nim @@ -1028,9 +1028,9 @@ proc back*[T](a: GMat4[T]): GVec3[T] {.inline.} = proc left*[T](a: GMat4[T]): GVec3[T] {.inline.} = ## Vector facing +X. - result.x = a[0, 0] - result.y = a[0, 1] - result.z = a[0, 2] + result.x = -a[0, 0] + result.y = -a[0, 1] + result.z = -a[0, 2] proc right*[T](a: GMat4[T]): GVec3[T] {.inline.} = ## Vector facing -X. @@ -1056,6 +1056,13 @@ proc `pos=`*[T](a: var GMat4[T], pos: GVec3[T]) = a[3, 1] = pos.y a[3, 2] = pos.z +proc `*`*[T](a, b: GMat2[T]): GMat2[T] = + result[0, 0] = b[0, 0] * a[0, 0] + b[0, 1] * a[1, 0] + result[0, 1] = b[0, 0] * a[0, 1] + b[0, 1] * a[1, 1] + + result[1, 0] = b[1, 0] * a[0, 0] + b[1, 1] * a[1, 0] + result[1, 1] = b[1, 0] * a[0, 1] + b[1, 1] * a[1, 1] + proc `*`*[T](a, b: GMat3[T]): GMat3[T] = result[0, 0] = b[0, 0] * a[0, 0] + b[0, 1] * a[1, 0] + b[0, 2] * a[2, 0] result[0, 1] = b[0, 0] * a[0, 1] + b[0, 1] * a[1, 1] + b[0, 2] * a[2, 1] @@ -1160,8 +1167,15 @@ proc `*`*[T](a: GMat4[T], b: GVec4[T]): GVec4[T] = a[0, 3] * b.x + a[1, 3] * b.y + a[2, 3] * b.z + a[3, 3] * b.w ) +proc transpose*[T](a: GMat2[T]): GMat2[T] = + ## Return a transpose of the matrix. + gmat2[T]( + a[0, 0], a[1, 0], + a[0, 1], a[1, 1] + ) + proc transpose*[T](a: GMat3[T]): GMat3[T] = - ## Return an transpose of the matrix. + ## Return a transpose of the matrix. gmat3[T]( a[0, 0], a[1, 0], a[2, 0], a[0, 1], a[1, 1], a[2, 1], @@ -1177,6 +1191,10 @@ proc transpose*[T](a: GMat4[T]): GMat4[T] = a[0, 3], a[1, 3], a[2, 3], a[3, 3] ) +proc determinant*[T](a: GMat2[T]): T = + ## Compute a determinant of the matrix. + a[0, 0] * a[1, 1] - a[1, 0] * a[0, 1] + proc determinant*[T](a: GMat3[T]): T = ## Compute a determinant of the matrix. ( @@ -1213,6 +1231,14 @@ proc determinant*[T](a: GMat4[T]): T = a20*a01*a12*a33 - a00*a21*a12*a33 - a10*a01*a22*a33 + a00*a11*a22*a33 ) +proc inverse*[T](a: GMat2[T]): GMat2[T] = + ## Return an inverse of the matrix. + let invDet = 1 / a.determinant + result[0, 0] = +a[1, 1] * invDet + result[0, 1] = -a[0, 1] * invDet + result[1, 0] = -a[1, 0] * invDet + result[1, 1] = +a[0, 0] * invDet + proc inverse*[T](a: GMat3[T]): GMat3[T] = ## Return an inverse of the matrix. let @@ -1328,8 +1354,8 @@ proc rotate*[T](angle: T): GMat3[T] = sin = sin(angle) cos = cos(angle) gmat3[T]( - cos, -sin, 0, - sin, cos, 0, + cos, sin, 0, + -sin, cos, 0, 0, 0, 1 ) @@ -1337,7 +1363,7 @@ proc rotationOnly*[T](a: GMat4[T]): GMat4[T] {.inline.} = ## Clears the positional component and returns rotation only. ## Assumes matrix has not been scaled. result = a - result.pos = gvec3(0, 0, 0) + result.pos = gvec3[T](0, 0, 0) proc rotateX*[T](angle: T): GMat4[T] = ## Return a rotation matrix around X with angle. @@ -1349,11 +1375,11 @@ proc rotateX*[T](angle: T): GMat4[T] = result[1, 0] = 0 result[1, 1] = cos(angle) - result[1, 2] = -sin(angle) + result[1, 2] = sin(angle) result[1, 3] = 0 result[2, 0] = 0 - result[2, 1] = sin(angle) + result[2, 1] = -sin(angle) result[2, 2] = cos(angle) result[2, 3] = 0 @@ -1367,7 +1393,7 @@ proc rotateY*[T](angle: T): GMat4[T] = ## All angles assume radians. result[0, 0] = cos(angle) result[0, 1] = 0 - result[0, 2] = sin(angle) + result[0, 2] = -sin(angle) result[0, 3] = 0 result[1, 0] = 0 @@ -1375,7 +1401,7 @@ proc rotateY*[T](angle: T): GMat4[T] = result[1, 2] = 0 result[1, 3] = 0 - result[2, 0] = -sin(angle) + result[2, 0] = sin(angle) result[2, 1] = 0 result[2, 2] = cos(angle) result[2, 3] = 0 @@ -1389,11 +1415,11 @@ proc rotateZ*[T](angle: T): GMat4[T] = ## Return a rotation matrix around Z with angle. ## All angles assume radians. result[0, 0] = cos(angle) - result[0, 1] = -sin(angle) + result[0, 1] = sin(angle) result[0, 2] = 0 result[0, 3] = 0 - result[1, 0] = sin(angle) + result[1, 0] = -sin(angle) result[1, 1] = cos(angle) result[1, 2] = 0 result[1, 3] = 0 @@ -1437,17 +1463,15 @@ proc toAngles*[T](m: GMat4[T]): GVec3[T] = ## roll (z rotation) ## Assumes matrix has not been scaled. ## All angles assume radians. - result.x = arcsin(m[2,1]) - if result.x > PI/2: - # Degenerate case over north pole. - result.y = arctan2(m[0, 2], m[0, 0]) - elif result.x < -PI/2: - # Degenerate case over south pole. - result.y = arctan2(m[0, 2], m[0, 0]) + let sy = clamp(-m[2, 1], T(-1), T(1)) + result.x = arcsin(sy) + if abs(sy) > T(0.9999999): + # Degenerate case (gimbal lock). + result.y = arctan2(-m[0, 2], m[0, 0]) else: # Normal case. - result.y = -arctan2(m[2, 0], m[2, 2]) - result.z = -arctan2(m[0, 1], m[1, 1]) + result.y = arctan2(m[2, 0], m[2, 2]) + result.z = arctan2(m[0, 1], m[1, 1]) proc fromAngles*[T](a: GVec3[T]): GMat4[T] = ## Takes a vector containing Euler angles and returns a matrix. @@ -1515,10 +1539,7 @@ proc ortho*[T](left, right, bottom, top, near, far: T): GMat4[T] = result[3, 2] = T(-(far + near) / fn) result[3, 3] = 1 -proc lookAt*[T](eye, center, up: GVec3[T]): GMat4[T] - {.deprecated: "Wrong coordinate system. " & - "Use toAngles(eye, center).fromAngles() instead to get " & - "right-handed-z-forward coordinate system".} = +proc lookAt*[T](eye, center, up: GVec3[T]): GMat4[T] = ## Create a matrix that would convert eye pos to looking at center. let eyex = eye[0] @@ -1599,12 +1620,9 @@ proc lookAt*[T](eye, center, up: GVec3[T]): GMat4[T] result[3, 2] = -(z0 * eyex + z1 * eyey + z2 * eyez) result[3, 3] = 1 -proc lookAt*[T](eye, center: GVec3[T]): GMat4[T] - {.deprecated: "Wrong coordinate system. " & - "Use toAngles(eye, center).fromAngles() instead to get " & - "right-handed-z-forward coordinate system".} = - ## Look center from eye with default UP vector. - lookAt(eye, center, gvec3(T(0), 0, 1)) +proc lookAt*[T](eye, center: GVec3[T]): GMat4[T] = + ## Look at center from eye with default UP vector. + lookAt(eye, center, gvec3(T(0), 1, 0)) proc angle*[T](a: GVec2[T]): T = ## Angle of a Vec2. @@ -1649,21 +1667,18 @@ proc fromAxisAngle*[T](axis: GVec3[T], angle: T): GVec4[T] = proc toAxisAngle*[T](q: GVec4[T]): (GVec3[T], T) = ## Convert a quaternion to axis and angle. - let cosAngle = q.w - let angle = arccos(cosAngle) * 2.0 - var - sinAngle = sqrt(1.0 - cosAngle * cosAngle) - axis: GVec4[T] - - if abs(sinAngle) < 0.0005: - sinAngle = 1.0 - - axis.x = [ - q.x / sinAngle, - q.y / sinAngle, - q.z / sinAngle - ] - return (axis, angle) + let w = clamp(q.w, T(-1), T(1)) + let angle = arccos(w) * 2 + let sinAngle = sqrt(1 - w * w) + if abs(sinAngle) < T(0.0005): + return (gvec3[T](1, 0, 0), angle) + return (gvec3[T](q.x / sinAngle, q.y / sinAngle, q.z / sinAngle), angle) + +proc quatInverse*[T](q: GVec4[T]): GVec4[T] = + ## Return the inverse of a quaternion. + ## For unit quaternions this is the conjugate. + let d = dot(q, q) + gvec4[T](-q.x / d, -q.y / d, -q.z / d, q.w / d) proc orthogonal*[T](v: GVec3[T]): GVec3[T] = ## Returns orthogonal vector to given vector. @@ -1699,8 +1714,8 @@ proc fromTwoVectors*[T](a, b: GVec3[T]): GVec4[T] = let half = normalize(u + v) - q = cross(u, half) - w = dot(u, half) + q = cross(v, half) + w = dot(v, half) return gvec4(q.x, q.y, q.z, w) proc nlerp*(a: Quat, b: Quat, v: float32): Quat = @@ -1709,6 +1724,28 @@ proc nlerp*(a: Quat, b: Quat, v: float32): Quat = else: (a * (1.0 - v) + b * v).normalize() +proc slerp*[T](a, b: GVec4[T], t: T): GVec4[T] = + ## Spherical linear interpolation between two quaternions. + var z = b + var cosTheta = dot(a, b) + + # Take short path. + if cosTheta < 0: + z = -b + cosTheta = -cosTheta + + # Linear interpolation when nearly parallel to avoid division by zero. + if cosTheta > 1 - T(1e-6): + return gvec4( + a.x + (z.x - a.x) * t, + a.y + (z.y - a.y) * t, + a.z + (z.z - a.z) * t, + a.w + (z.w - a.w) * t, + ) + else: + let angle = arccos(cosTheta) + return (sin((1 - t) * angle) * a + sin(t * angle) * z) / sin(angle) + proc quat*[T](m: GMat4[T]): GVec4[T] = ## Create a quaternion from matrix. let @@ -1724,30 +1761,51 @@ proc quat*[T](m: GMat4[T]): GVec4[T] = m21 = m[2, 1] m22 = m[2, 2] + fourXSquaredMinus1 = m00 - m11 - m22 + fourYSquaredMinus1 = m11 - m00 - m22 + fourZSquaredMinus1 = m22 - m00 - m11 + fourWSquaredMinus1 = m00 + m11 + m22 + var q: GVec4[T] - t: T - - if m22 < 0: - if m00 > m11: - t = 1 + m00 - m11 - m22 - q = gvec4(t, m01 + m10, m20 + m02, m21 - m12) - else: - t = 1 - m00 + m11 - m22 - q = gvec4(m01 + m10, t, m12 + m21, m02 - m20) + biggestIndex = 0 + fourBiggestSquaredMinus1 = fourWSquaredMinus1 + if fourXSquaredMinus1 > fourBiggestSquaredMinus1: + fourBiggestSquaredMinus1 = fourXSquaredMinus1 + biggestIndex = 1 + if fourYSquaredMinus1 > fourBiggestSquaredMinus1: + fourBiggestSquaredMinus1 = fourYSquaredMinus1 + biggestIndex = 2 + if fourZSquaredMinus1 > fourBiggestSquaredMinus1: + fourBiggestSquaredMinus1 = fourZSquaredMinus1 + biggestIndex = 3 + + let biggestVal = sqrt(fourBiggestSquaredMinus1 + T(1)) * T(0.5) + let mult = T(0.25) / biggestVal + + case biggestIndex + of 0: + q.w = biggestVal + q.x = (m12 - m21) * mult + q.y = (m20 - m02) * mult + q.z = (m01 - m10) * mult + of 1: + q.w = (m12 - m21) * mult + q.x = biggestVal + q.y = (m01 + m10) * mult + q.z = (m20 + m02) * mult + of 2: + q.w = (m20 - m02) * mult + q.x = (m01 + m10) * mult + q.y = biggestVal + q.z = (m12 + m21) * mult else: - if m00 < - m11: - t = 1 - m00 - m11 + m22 - q = gvec4(m20 + m02, m12 + m21, t, m10 - m01) - else: - t = 1 + m00 + m11 + m22 - q = gvec4(m21 - m12, m02 - m20, m10 - m01, t) - q = q * (0.5 / sqrt(t)) - - if abs(q.length - 1.0) > 0.001: - return gvec4(T(0), 0, 0, 1) + q.w = (m01 - m10) * mult + q.x = (m20 + m02) * mult + q.y = (m12 + m21) * mult + q.z = biggestVal - return q + result = q proc mat4*[T](q: GVec4[T]): GMat4[T] = let @@ -1764,17 +1822,17 @@ proc mat4*[T](q: GVec4[T]): GMat4[T] = zw = q.z * q.w result[0, 0] = 1 - 2 * (yy + zz) - result[0, 1] = 0 + 2 * (xy - zw) - result[0, 2] = 0 + 2 * (xz + yw) + result[0, 1] = 0 + 2 * (xy + zw) + result[0, 2] = 0 + 2 * (xz - yw) result[0, 3] = 0 - result[1, 0] = 0 + 2 * (xy + zw) + result[1, 0] = 0 + 2 * (xy - zw) result[1, 1] = 1 - 2 * (xx + zz) - result[1, 2] = 0 + 2 * (yz - xw) + result[1, 2] = 0 + 2 * (yz + xw) result[1, 3] = 0 - result[2, 0] = 0 + 2 * (xz - yw) - result[2, 1] = 0 + 2 * (yz + xw) + result[2, 0] = 0 + 2 * (xz + yw) + result[2, 1] = 0 + 2 * (yz - xw) result[2, 2] = 1 - 2 * (xx + yy) result[2, 3] = 0 @@ -1859,8 +1917,9 @@ proc quatRotate*[T](q: GVec4[T], v: GVec3[T]): GVec3[T] = ## Rotate a vector directly by a quaternion without building a matrix. let qv = gvec3[T](q.x, q.y, q.z) - t = cross(v, qv) * 2 - v + q.w * t + cross(t, qv) + uv = cross(qv, v) + uuv = cross(qv, uv) + v + (uv * q.w + uuv) * 2 proc `*`*[T](a: GVec4[T], b: GVec3[T]): GVec3[T] {.inline.} = ## Rotate a vector directly by a quaternion. diff --git a/tests/test.nim b/tests/test.nim index 08ebbf5..9382c52 100644 --- a/tests/test.nim +++ b/tests/test.nim @@ -1,1190 +1,1404 @@ import - std/random, + std/[math, random, unittest], vmath randomize(1234) -block: - # Test ~=. - doAssert 1.0 ~= 1.0 - doAssert 0.0 ~= 0.0 - doAssert -1.0 ~= -1.0 - doAssert not(0.1 ~= 0.2) - doAssert not(0.01 ~= 0.02) - doAssert not(0.001 ~= 0.002) - doAssert not(0.0001 ~= 0.0002) - doAssert not(0.00001 ~= 0.00002) - - # Diff < epsilon. - doAssert 0.000001 ~= 0.000002 - doAssert -0.000001 ~= -0.000002 - - doAssert vec2(1.0, 2.0) ~= vec2(1.0, 2.0) - doAssert vec3(1.0, 2.0, 3.0) ~= vec3(1.0, 2.0, 3.0) - doAssert vec4(1.0, 2.0, 3.0, 4.0) ~= vec4(1.0, 2.0, 3.0, 4.0) - doAssert quat(1.0, 2.0, 3.0, 4.0) ~= quat(1.0, 2.0, 3.0, 4.0) - - doAssert dvec2(1) ~= dvec2(1) - doAssert dvec4(1, 2, 3, 4).xy ~= dvec2(1, 2) - - when compiles(1 ~= 1): - doAssert false - -block: - # Test simple functions. - doAssert between(0.5, 0, 1) - doAssert not between(1.5, 0, 1) - - doAssert sign(-1.0) == -1.0 - doAssert sign(0.0) == 1.0 - doAssert sign(1.0) == 1.0 - - block: - proc quantize2(v, n: float32): float32 = - ## Makes v be multiple of n. Rounding to integer quantize by 1.0. - sign(v) * trunc(abs(v) / n) * n - - let n = 1.float32 / 10 - - for _ in 0 ..< 10_000: - let f = rand(-100.float32 .. 100.float32) - doAssert quantize(f, n) == quantize2(f, n) - - doAssert quantize(1.23456789, 1.0) ~= 1 - doAssert quantize(1.23456789, 0.1) ~= 1.2 - doAssert quantize(1.23456789, 0.01) ~= 1.23 - doAssert quantize(-1.23456789, 0.01) ~= -1.23 - - doAssert fract(0.0) ~= 0.0 - doAssert fract(3.14) ~= 0.14 - doAssert fract(-3.14) ~= 0.14 - doAssert fract(1.23456789) ~= 0.23456789 - doAssert fract(-1.23456789) ~= 0.23456789 - - doAssert mix(0.0, 1.0, 0.5) ~= 0.5 - doAssert mix(0.0, 10.0, 0.5) ~= 5.0 - doAssert mix(0.0, 100.0, 0.5) ~= 50.0 - doAssert mix(-1.0, 1.0, 0.25) ~= -0.5 - doAssert mix(-10.0, 10.0, 0.25) ~= -5.0 - doAssert mix(-100.0, 100.0, 0.25) ~= -50.0 - - doAssert mix(0.0, 1.0, 0.5) ~= 0.5 - doAssert mix(0.0, 10.0, 0.5) ~= 5.0 - doAssert mix(0.0, 100.0, 0.5) ~= 50.0 - doAssert mix(-1.0, 1.0, 0.25) ~= -0.5 - doAssert mix(-10.0, 10.0, 0.25) ~= -5.0 - doAssert mix(-100.0, 100.0, 0.25) ~= -50.0 - - doAssert fixAngle(0.1) ~= 0.1 - doAssert fixAngle(1.1) ~= 1.1 - doAssert fixAngle(2.1) ~= 2.1 - doAssert fixAngle(3.1) ~= 3.1 - doAssert fixAngle(4.1) ~= -2.183185577392578 - doAssert fixAngle(-0.1) ~= -0.1 - doAssert fixAngle(-1.1) ~= -1.1 - doAssert fixAngle(-2.1) ~= -2.1 - doAssert fixAngle(-3.1) ~= -3.1 - doAssert fixAngle(-4.1) ~= 2.183185577392578 - - doAssert angleBetween(0.0, 1.0) ~= 1.0 - doAssert angleBetween(0.0, PI) ~= PI - doAssert angleBetween(0.0, PI + 0.2) ~= (-PI + 0.2) - doAssert angleBetween(0.1, 0.2) ~= 0.1 - doAssert angleBetween(0.1, 0.2 + PI*2) ~= 0.1 - doAssert angleBetween(0.1, 0.2 - PI*2) ~= 0.1 - doAssert angleBetween(0.1 + PI*2, 0.2) ~= 0.1 - doAssert angleBetween(0.1 - PI*2, 0.2) ~= 0.1 - doAssert angleBetween(0.2, 0.1) ~= -0.1 - doAssert angleBetween(0.2, 0.1 - PI*2) ~= -0.1 - doAssert angleBetween(0.2, 0.1 + PI*2) ~= -0.1 - doAssert angleBetween(0.2 + PI*2, 0.1) ~= -0.1 - doAssert angleBetween(0.2 - PI*2, 0.1) ~= -0.1 - - doAssert turnAngle(0.0, PI, 0.5) ~= 0.5 - doAssert turnAngle(0.5, PI, 3.5) ~= PI - - proc isNaNSlow(f: SomeFloat): bool = - ## Returns true if number is a NaN. - f.classify notin {fcNormal, fcZero, fcSubnormal} - - doAssert isNaNSlow(0.3) == false - doAssert isNaNSlow(0.0) == false - doAssert isNaNSlow(0.3/0.0) == true - doAssert isNaNSlow(-0.3/0.0) == true - doAssert isNaNSlow(5.0e-324) == false - - doAssert isNan(float32(0.3)) == false - doAssert isNan(float32(0.0)) == false - doAssert isNan(float32(0.3/0.0)) == true - doAssert isNan(float32(-0.3/0.0)) == true - doAssert isNan(float32(5.0e-324)) == false - - doAssert isNan(float64(0.3)) == false - doAssert isNan(float64(0.0)) == false - doAssert isNan(float64(0.3/0.0)) == true - doAssert isNan(float64(-0.3/0.0)) == true - doAssert isNan(float64(5.0e-324)) == false - -block: - when not defined(js): - # Test vec2 cast. - var v = vec2(1.0, 2.0) - var a = cast[array[2, float32]](v) - doAssert a[0] ~= 1.0 - doAssert a[1] ~= 2.0 - -block: - # Test position assignment - var - v2 = vec2(0) - v3 = vec3(0) - v4 = vec4(0) - v2[0] = 1.0 - v2[1] = 2.0 - doAssert v2 ~= vec2(1, 2) - v3[0] = 1.0 - v3[1] = 2.0 - v3[2] = 3.0 - doAssert v3 ~= vec3(1, 2, 3) - v4[0] = 1.0 - v4[1] = 2.0 - v4[2] = 3.0 - v4[3] = 4.0 - doAssert v4 ~= vec4(1, 2, 3, 4) - -block: - # Test vec2 constructor. - doAssert vec2(PI, PI) ~= vec2(PI) - -block: - # Test basic vector vec2. - var a = vec2(1, 2) - var b = vec2(7, 6) - var n = 13.7 - doAssert a + b ~= vec2(8.0, 8.0) - doAssert a - b ~= vec2(-6.0, -4.0) - doAssert a * n ~= vec2(13.7, 27.4) - doAssert a / n ~= vec2(0.0729927, 0.1459854) - a += b - doAssert a ~= vec2(8.0, 8.0) - a -= b - doAssert a ~= vec2(1.0, 2.0) - a *= n - doAssert a ~= vec2(13.7, 27.4) - a /= n - doAssert a ~= vec2(1.0, 2.0) - -block: - # Test basic vector vec3. - var a = vec3(1, 2, 3) - var b = vec3(7, 6, 5) - var n = 13.7 - doAssert a + b ~= vec3(8.0, 8.0, 8.0) - doAssert a - b ~= vec3(-6.0, -4.0, -2.0) - doAssert a * n ~= vec3(13.69999981, 27.39999962, 41.09999847) - doAssert a / n ~= vec3(0.07299270, 0.14598541, 0.21897811) - a += b - doAssert a ~= vec3(8.0, 8.0, 8.0) - a -= b - doAssert a ~= vec3(1.0, 2.0, 3.0) - a *= n - doAssert a ~= vec3(13.69999981, 27.39999962, 41.09999847) - a /= n - doAssert a ~= vec3(1.0, 2.0, 3.0) - -block: - # Test basic vector vec4. - var a = vec4(1, 2, 3, 4) - var b = vec4(7, 6, 5, 4) - var n = 13.7 - doAssert a + b ~= vec4(8.0, 8.0, 8.0, 8.0) - doAssert a - b ~= vec4(-6.0, -4.0, -2.0, 0.0) - doAssert a * n ~= vec4(13.69999981, 27.39999962, 41.09999847, 54.79999924) - doAssert a / n ~= vec4(0.07299270, 0.14598541, 0.21897811, 0.29197082) - a += b - doAssert a ~= vec4(8.0, 8.0, 8.0, 8.0) - a -= b - doAssert a ~= vec4(1.0, 2.0, 3.0, 4.0) - a *= n - doAssert a ~= vec4(13.69999981, 27.39999962, 41.09999847, 54.79999924) - a /= n - doAssert a ~= vec4(1.0, 2.0, 3.0, 4.0) - -block: - # Test all type constructors compile +suite "approximate equality": + test "float ~=": + check 1.0 ~= 1.0 + check 0.0 ~= 0.0 + check -1.0 ~= -1.0 + check not(0.1 ~= 0.2) + check not(0.01 ~= 0.02) + check not(0.001 ~= 0.002) + check not(0.0001 ~= 0.0002) + check not(0.00001 ~= 0.00002) + check 0.000001 ~= 0.000002 + check -0.000001 ~= -0.000002 + + test "vec ~=": + check vec2(1.0, 2.0) ~= vec2(1.0, 2.0) + check vec3(1.0, 2.0, 3.0) ~= vec3(1.0, 2.0, 3.0) + check vec4(1.0, 2.0, 3.0, 4.0) ~= vec4(1.0, 2.0, 3.0, 4.0) + check quat(1.0, 2.0, 3.0, 4.0) ~= quat(1.0, 2.0, 3.0, 4.0) + check dvec2(1) ~= dvec2(1) + check dvec4(1, 2, 3, 4).xy ~= dvec2(1, 2) + + test "int ~= should not compile": + check not compiles(1 ~= 1) + +suite "scalar utilities": + test "between": + check between(0.5, 0, 1) + check not between(1.5, 0, 1) + + test "sign": + check sign(-1.0) == -1.0 + check sign(0.0) == 1.0 + check sign(1.0) == 1.0 + + test "quantize": + check quantize(1.23456789, 1.0) ~= 1 + check quantize(1.23456789, 0.1) ~= 1.2 + check quantize(1.23456789, 0.01) ~= 1.23 + check quantize(-1.23456789, 0.01) ~= -1.23 + + test "fract": + check fract(0.0) ~= 0.0 + check fract(3.14) ~= 0.14 + check fract(-3.14) ~= 0.14 + check fract(1.23456789) ~= 0.23456789 + + test "mix": + check mix(0.0, 1.0, 0.5) ~= 0.5 + check mix(0.0, 10.0, 0.5) ~= 5.0 + check mix(-1.0, 1.0, 0.25) ~= -0.5 + + test "fixAngle": + check fixAngle(0.1) ~= 0.1 + check fixAngle(3.1) ~= 3.1 + check fixAngle(4.1) ~= -2.183185577392578 + check fixAngle(-4.1) ~= 2.183185577392578 + + test "angleBetween": + check angleBetween(0.0, 1.0) ~= 1.0 + check angleBetween(0.0, PI) ~= PI + check angleBetween(0.1, 0.2) ~= 0.1 + check angleBetween(0.1, 0.2 + PI*2) ~= 0.1 + check angleBetween(0.2, 0.1) ~= -0.1 + + test "isNan": + check vmath.isNan(float32(0.3)) == false + check vmath.isNan(float32(0.0)) == false + check vmath.isNan(float32(0.3/0.0)) == true + check vmath.isNan(float64(0.3/0.0)) == true + +suite "vector memory layout": + test "vec2 cast to array": + when not defined(js): + let v = vec2(1.0, 2.0) + let a = cast[array[2, float32]](v) + check a[0] == 1.0f + check a[1] == 2.0f + + test "vec3 cast to array": + when not defined(js): + let v = vec3(1.0, 2.0, 3.0) + let a = cast[array[3, float32]](v) + check a[0] == 1.0f + check a[1] == 2.0f + check a[2] == 3.0f + + test "vec4 cast to array": + when not defined(js): + let v = vec4(1.0, 2.0, 3.0, 4.0) + let a = cast[array[4, float32]](v) + check a[0] == 1.0f + check a[1] == 2.0f + check a[2] == 3.0f + check a[3] == 4.0f + + test "vec component access matches array index": + let v2 = vec2(10, 20) + check v2.x == v2[0] + check v2.y == v2[1] + + let v3 = vec3(10, 20, 30) + check v3.x == v3[0] + check v3.y == v3[1] + check v3.z == v3[2] + + let v4 = vec4(10, 20, 30, 40) + check v4.x == v4[0] + check v4.y == v4[1] + check v4.z == v4[2] + check v4.w == v4[3] + + test "vec component assignment": + var v2 = vec2(0) + v2[0] = 1.0; v2[1] = 2.0 + check v2 ~= vec2(1, 2) + + var v3 = vec3(0) + v3[0] = 1.0; v3[1] = 2.0; v3[2] = 3.0 + check v3 ~= vec3(1, 2, 3) + + var v4 = vec4(0) + v4[0] = 1.0; v4[1] = 2.0; v4[2] = 3.0; v4[3] = 4.0 + check v4 ~= vec4(1, 2, 3, 4) + +suite "mat2 memory layout and element access": + test "mat2 cast to flat array is column-major": + when not defined(js): + # Memory: [col0_row0, col0_row1, col1_row0, col1_row1] + let m = cast[Mat2]([1.0f, 2.0f, 3.0f, 4.0f]) + let a = cast[array[4, float32]](m) + check a[0] == 1.0f # col 0 row 0 + check a[1] == 2.0f # col 0 row 1 + check a[2] == 3.0f # col 1 row 0 + check a[3] == 4.0f # col 1 row 1 + + test "mat2 [row, col] element access": + when not defined(js): + let m = cast[Mat2]([1.0f, 2.0f, 3.0f, 4.0f]) + # [row, col] indexing into column-major memory + check m[0, 0] == 1.0f # arr[0*2+0] = arr[0] + check m[0, 1] == 2.0f # arr[0*2+1] = arr[1] + check m[1, 0] == 3.0f # arr[1*2+0] = arr[2] + check m[1, 1] == 4.0f # arr[1*2+1] = arr[3] + + test "mat2 element assignment": + var m = mat2() + m[0, 0] = 5.0; m[0, 1] = 6.0 + m[1, 0] = 7.0; m[1, 1] = 8.0 + check m[0, 0] == 5.0f + check m[0, 1] == 6.0f + check m[1, 0] == 7.0f + check m[1, 1] == 8.0f + + test "mat2 identity": + let m = mat2() + check m[0, 0] == 1.0f + check m[0, 1] == 0.0f + check m[1, 0] == 0.0f + check m[1, 1] == 1.0f + + test "mat2 scalar constructor": + let m = mat2(1, 2, 3, 4) + check m[0, 0] == 1.0f + check m[0, 1] == 2.0f + check m[1, 0] == 3.0f + check m[1, 1] == 4.0f + + test "mat2 vector column constructor": + let m = mat2(vec2(1, 2), vec2(3, 4)) + check m[0, 0] == 1.0f + check m[0, 1] == 2.0f + check m[1, 0] == 3.0f + check m[1, 1] == 4.0f + +suite "mat2 operations": let - _ = bvec2(true, false) - _ = bvec3(true, false, true) - _ = bvec4(true, false, true, false) - - _ = ivec2(-1, 2) - _ = ivec3(-1, 2, 3) - _ = ivec4(-1, 2, 3, 4) - - _ = uvec2(1, 2) - _ = uvec3(1, 2, 3) - _ = uvec4(1, 2, 3, 4) - - _ = vec2(1.0, 2.0) - _ = vec3(1.0, 2.0, 3.0) - _ = vec4(1.0, 2.0, 3.0, 4.0) - - _ = dvec2(1.0, 2.0) - _ = dvec3(1.0, 2.0, 3.0) - _ = dvec4(1.0, 2.0, 3.0, 4.0) - - _ = bvec2(true) - _ = bvec3(true) - _ = bvec4(true) - - _ = ivec2(-1) - _ = ivec3(-1) - _ = ivec4(-1) - - _ = uvec2(1) - _ = uvec3(1) - _ = uvec4(1) - - _ = vec2(1.0) - _ = vec3(1.0) - _ = vec4(1.0) - - _ = dvec2(1.0) - _ = dvec3(1.0) - _ = dvec4(1.0) - - _ = bvec2() - _ = bvec3() - _ = bvec4() - - _ = ivec2() - _ = ivec3() - _ = ivec4() - - _ = uvec2() - _ = uvec3() - _ = uvec4() - - _ = vec2() - _ = vec3() - _ = vec4() - - _ = dvec2() - _ = dvec3() - _ = dvec4() - - var a = vec3(vec2(1, 2), 3) - doAssert a == vec3(1, 2, 3) + a = when defined(js): + mat2(1, 2, 3, 4) + else: + cast[Mat2]([1.0f, 2.0f, 3.0f, 4.0f]) + b = when defined(js): + mat2(5, -6, 7, -8) + else: + cast[Mat2]([5.0f, -6.0f, 7.0f, -8.0f]) + + test "mat2 multiply": + let ab = a * b + let ba = b * a + check not(ab ~= ba) # non-commutative + # identity * a = a + check mat2() * a ~= a + check a * mat2() ~= a + + test "mat2 transpose": + let t = transpose(a) + check t[0, 0] == a[0, 0] + check t[0, 1] == a[1, 0] + check t[1, 0] == a[0, 1] + check t[1, 1] == a[1, 1] + # double transpose = original + check transpose(transpose(a)) ~= a + + test "mat2 determinant": + check determinant(a) == 1.0f * 4.0f - 3.0f * 2.0f # ad - bc = -2 + + test "mat2 inverse": + let inv = inverse(a) + check a * inv ~= mat2() + check inv * a ~= mat2() + + test "mat2 * vec2": + let v = vec2(1.25, -2.5) + let result = a * v + # Verify by manual computation + check result.x ~= (a[0, 0] * v.x + a[1, 0] * v.y) + check result.y ~= (a[0, 1] * v.x + a[1, 1] * v.y) + +suite "mat3 memory layout and element access": + test "mat3 cast to flat array is column-major": + when not defined(js): + # 9 floats: col0(3), col1(3), col2(3) + let m = cast[Mat3]([1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f]) + let a = cast[array[9, float32]](m) + check a[0] == 1.0f # col 0 row 0 + check a[1] == 2.0f # col 0 row 1 + check a[2] == 3.0f # col 0 row 2 + check a[3] == 4.0f # col 1 row 0 + check a[4] == 5.0f # col 1 row 1 + check a[5] == 6.0f # col 1 row 2 + check a[6] == 7.0f # col 2 row 0 + check a[7] == 8.0f # col 2 row 1 + check a[8] == 10.0f # col 2 row 2 + + test "mat3 [row, col] element access": + when not defined(js): + let m = cast[Mat3]([1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f]) + check m[0, 0] == 1.0f + check m[0, 1] == 2.0f + check m[0, 2] == 3.0f + check m[1, 0] == 4.0f + check m[1, 1] == 5.0f + check m[1, 2] == 6.0f + check m[2, 0] == 7.0f + check m[2, 1] == 8.0f + check m[2, 2] == 10.0f + + test "mat3 identity": + let m = mat3() + for r in 0 .. 2: + for c in 0 .. 2: + if r == c: + check m[r, c] == 1.0f + else: + check m[r, c] == 0.0f + + test "mat3 element assignment": + var m = mat3() + m[1, 2] = 42.0 + check m[1, 2] == 42.0f + +suite "mat3 operations": + let + a = when defined(js): + mat3(1, 2, 3, 4, 5, 6, 7, 8, 10) + else: + cast[Mat3]([1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 10.0f]) + b = when defined(js): + mat3(-1, 3, 5, 7, -2, 4, 6, 8, -3) + else: + cast[Mat3]([-1.0f, 3.0f, 5.0f, 7.0f, -2.0f, 4.0f, 6.0f, 8.0f, -3.0f]) + + test "mat3 multiply": + check mat3() * a ~= a + check a * mat3() ~= a + check not(a * b ~= b * a) + + test "mat3 transpose": + let t = transpose(a) + for r in 0 .. 2: + for c in 0 .. 2: + check t[r, c] == a[c, r] + check transpose(transpose(a)) ~= a + + test "mat3 determinant non-zero": + # [1..8, 10] has det = -3 + check abs(determinant(a) - (-3.0f)) < 0.001f + + test "mat3 inverse": + let inv = inverse(a) + check a * inv ~= mat3() + check inv * a ~= mat3() + + test "mat3 * vec3": + let v = vec3(1.0, -2.0, 3.0) + let result = a * v + check result.x ~= (a[0, 0] * v.x + a[1, 0] * v.y + a[2, 0] * v.z) + check result.y ~= (a[0, 1] * v.x + a[1, 1] * v.y + a[2, 1] * v.z) + check result.z ~= (a[0, 2] * v.x + a[1, 2] * v.y + a[2, 2] * v.z) + + test "mat3 * vec2 (2D homogeneous)": + let v = vec2(3.0, -1.5) + let result = a * v + # Should treat vec2 as vec3(x, y, 1) and return xy + let full = a * vec3(v.x, v.y, 1.0) + check result.x ~= full.x + check result.y ~= full.y + +suite "mat3 2D constructors": + test "scale2D": + let s = scale(vec2(2.0, 3.0)) + check s[0, 0] == 2.0f + check s[1, 1] == 3.0f + check s[2, 2] == 1.0f + check s[1, 0] == 0.0f + # Scaling a point + let v = s * vec2(5.0, 10.0) + check v ~= vec2(10.0, 30.0) + + test "translate2D": + let t = translate(vec2(5.0, 10.0)) + check t[2, 0] == 5.0f + check t[2, 1] == 10.0f + let v = t * vec2(1.0, 2.0) + check v ~= vec2(6.0, 12.0) + + test "rotate2D": + let r = rotate(45.0f.toRadians) + # cos(45) ~= 0.707, sin(45) ~= 0.707 + check r[0, 0] ~= cos(45.0f.toRadians) + check r[0, 1] ~= sin(45.0f.toRadians) + check r[1, 0] ~= -sin(45.0f.toRadians) + check r[1, 1] ~= cos(45.0f.toRadians) + # Rotating (1, 0) by 90 degrees should give (0, 1) + let r90 = rotate(90.0f.toRadians) + check r90 * vec2(1.0, 0.0) ~= vec2(0.0, 1.0) + + test "2D transform composition": + let mat2d = translate(vec2(10, 20)) * rotate(45.toRadians) * scale(vec2(2)) + check mat2d ~= mat3( + 1.414213538169861, 1.414213538169861, 0.0, + -1.414213538169861, 1.414213538169861, 0.0, + 10.0, 20.0, 1.0 + ) + +suite "mat4 memory layout and element access": + test "mat4 cast to flat array is column-major": + when not defined(js): + let m = cast[Mat4]([ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + ]) + let a = cast[array[16, float32]](m) + # Column 0 + check a[0] == 1.0f; check a[1] == 2.0f; check a[2] == 3.0f; check a[3] == 4.0f + # Column 1 + check a[4] == 5.0f; check a[5] == 6.0f; check a[6] == 7.0f; check a[7] == 8.0f + # Column 2 + check a[8] == 9.0f; check a[9] == 10.0f; check a[10] == 11.0f; check a[11] == 12.0f + # Column 3 + check a[12] == 13.0f; check a[13] == 14.0f; check a[14] == 15.0f; check a[15] == 16.0f + + test "mat4 [row, col] element access matches flat column-major": + when not defined(js): + let m = cast[Mat4]([ + 1.0f, 2.0f, 3.0f, 4.0f, + 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, + 13.0f, 14.0f, 15.0f, 16.0f + ]) + let a = cast[array[16, float32]](m) + for row in 0 .. 3: + for col in 0 .. 3: + # m[row, col] should equal a[row * 4 + col] + check m[row, col] == a[row * 4 + col] + + test "mat4 identity": + let m = mat4() + for r in 0 .. 3: + for c in 0 .. 3: + if r == c: + check m[r, c] == 1.0f + else: + check m[r, c] == 0.0f + + test "mat4 constructors match": + let fromScalars = mat4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ) + let fromVecs = mat4( + vec4(1, 0, 0, 0), + vec4(0, 1, 0, 0), + vec4(0, 0, 1, 0), + vec4(0, 0, 0, 1) + ) + check fromScalars ~= fromVecs + check fromScalars ~= mat4() - var b = vec4(vec3(1, 2, 3), 4) - doAssert b == vec4(1, 2, 3, 4) - - var c = vec4(vec2(1, 2), vec2(3, 4)) - doAssert c == vec4(1, 2, 3, 4) - -block: - # test $ string functions - doAssert $bvec2(true, false) == "bvec2(true, false)" - doAssert $bvec3(true, false, true) == "bvec3(true, false, true)" - doAssert $bvec4(true, false, true, false) == "bvec4(true, false, true, false)" - - doAssert $ivec2(1, 2) == "ivec2(1, 2)" - doAssert $ivec3(1, 2, 3) == "ivec3(1, 2, 3)" - doAssert $ivec4(1, 2, 3, 4) == "ivec4(1, 2, 3, 4)" - - doAssert $uvec2(1, 2) == "uvec2(1, 2)" - doAssert $uvec3(1, 2, 3) == "uvec3(1, 2, 3)" - doAssert $uvec4(1, 2, 3, 4) == "uvec4(1, 2, 3, 4)" - - doAssert $vec2(1.0, 2.0) == "vec2(1.0, 2.0)" - doAssert $vec3(1.0, 2.0, 3.0) == "vec3(1.0, 2.0, 3.0)" - doAssert $vec4(1.0, 2.0, 3.0, 4.0) == "vec4(1.0, 2.0, 3.0, 4.0)" - - doAssert $dvec2(1.0, 2.0) == "dvec2(1.0, 2.0)" - doAssert $dvec3(1.0, 2.0, 3.0) == "dvec3(1.0, 2.0, 3.0)" - doAssert $dvec4(1.0, 2.0, 3.0, 4.0) == "dvec4(1.0, 2.0, 3.0, 4.0)" - -block: - # test swizzle vec - var a = vec2(1, 2) - doAssert a.x == 1.0 - doAssert a.y == 2.0 - doAssert a.yx == vec2(2, 1) - doAssert a.gr == vec2(2, 1) - doAssert a.ts == vec2(2, 1) - doAssert a.xxx == vec3(1, 1, 1) - - a.yx = vec2(-1, -2) - doAssert a == vec2(-2, -1) - - a.xx = vec2(-7, -3) - doAssert a == vec2(-3, -1) - - when compiles(a.xyzxyz): - doAssert false - - when compiles(a.z = 123): - doAssert false - - var b = vec4(1, 2, 3, 4) - doAssert b == vec4(1, 2, 3, 4) - b.wzyx = b - doAssert b == vec4(4, 3, 2, 1) - - b.g = 123 - doAssert b == vec4(4.0, 123.0, 2.0, 1.0) - -block: - # test swizzle dvec float64 - var a = dvec2(1, 2) - doAssert a.x == 1.0 - doAssert a.y == 2.0 - doAssert a.yx == dvec2(2, 1) - doAssert a.gr == dvec2(2, 1) - doAssert a.ts == dvec2(2, 1) - doAssert a.xxx == dvec3(1, 1, 1) - - a.yx = dvec2(-1, -2) - doAssert a == dvec2(-2, -1) - - a.xx = dvec2(-7, -3) - doAssert a == dvec2(-3, -1) - - when compiles(a.xyzxyz): - doAssert false - - when compiles(a.z = 123): - doAssert false - - var b = dvec4(1, 2, 3, 4) - doAssert b == dvec4(1, 2, 3, 4) - - b.g = 123 - doAssert b == dvec4(1.0, 123.0, 3.0, 4.0) - -block: - # test swizzle self-assignment - var a = dvec2(1, 2) - a.yx = a - doAssert a == dvec2(2, 1) - - var b = dvec3(1, 2, 3) - b.zyx = b - doAssert b == dvec3(3, 2, 1) - - var c = dvec4(1, 2, 3, 4) - c.wzyx = c - doAssert c == dvec4(4, 3, 2, 1) - -block: - # Test swizzle calls only once - var callCount = 0 - proc countsCalls(): Vec2 = - inc callCount - - doAssert countsCalls().yx == vec2(0, 0) - doAssert callCount == 1 - - callCount = 0 - doAssert vec2(0, 0) == countsCalls().yx - doAssert callCount == 1 - - var tmp: Vec2 - proc countsCalls2(): var Vec2 = - inc callCount - return tmp - - callCount = 0 - countsCalls2().yx = vec2(0, 0) - doAssert callCount == 1 - -block: - # Test swizzle with complex expressions - var a = [ - vec2(1, 2), - vec2(3, 4), - vec2(5, 6), - vec2(7, 8), - ] - var i = 0 - proc f(): var Vec2 = - # function with side effects - result = a[i] - inc i - - doAssert f().yx == vec2(2, 1) - doAssert f().gr == vec2(4, 3) - doAssert f().ts == vec2(6, 5) - doAssert f().yx == vec2(8, 7) - doAssert i == 4 - - i = 0 - f().yx = f() - doAssert a[0] == vec2(4, 3) - doAssert a[1] == vec2(3, 4) - doAssert i == 2 - - var b = [ - vec3(1, 2, 3), - vec3(4, 5, 6), - vec3(7, 8, 9), - ] - i = 0 - proc g(): var Vec3 = - # function with side effects - result = b[i] - inc i - - doAssert g().yxz == vec3(2, 1, 3) - doAssert g().bgr == vec3(6, 5, 4) - doAssert g().tps == vec3(8, 9, 7) - doAssert i == 3 - - i = 0 - g().yxz = g() - doAssert b[0] == vec3(5, 4, 6) - doAssert b[1] == vec3(4, 5, 6) - doAssert i == 2 - - var c = [ - vec4(1, 2, 3, 4), - vec4(5, 6, 7, 8), - ] - i = 0 - proc h(): var Vec4 = - # function with side effects - result = c[i] - inc i - - doAssert h().yxzw == vec4(2, 1, 3, 4) - doAssert h().tqsp == vec4(6, 8, 5, 7) - doAssert i == 2 - - i = 0 - h().wzyx = h() - doAssert c[0] == vec4(8, 7, 6, 5) - doAssert c[1] == vec4(5, 6, 7, 8) - doAssert i == 2 - - -block: - # Test basic mat constructors. - block: +suite "mat4 operations": + let + a = when defined(js): + mat4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) + else: + cast[Mat4]([ + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, + 9.0f, 10.0f, 11.0f, 12.0f, 13.0f, 14.0f, 15.0f, 16.0f + ]) + b = when defined(js): + mat4(-10, -20, -30, -40, 50, 60, 70, 80, 90, 100, 110, 120, 130, 140, 150, 160) + else: + cast[Mat4]([ + -10.0f, -20.0f, -30.0f, -40.0f, 50.0f, 60.0f, 70.0f, 80.0f, + 90.0f, 100.0f, 110.0f, 120.0f, 130.0f, 140.0f, 150.0f, 160.0f + ]) + + test "mat4 multiply identity": + check mat4() * a ~= a + check a * mat4() ~= a + + test "mat4 multiply non-commutative": + check not(a * b ~= b * a) + + test "mat4 transpose": + let t = transpose(a) + for r in 0 .. 3: + for c in 0 .. 3: + check t[r, c] == a[c, r] + check transpose(transpose(a)) ~= a + + test "mat4 inverse (float32, game transform)": + # Typical game transform: position + rotation + non-uniform scale + let m = translate(vec3(100, -50, 200)) * rotateZ(71.toRadians) * + rotateY(-23.toRadians) * rotateX(37.toRadians) * scale(vec3(2, 3, 4)) + let inv = inverse(m) + let product = m * inv + # float32 inverse of a full T*R*S matrix drifts ~1e-5, too loose for ~= (1e-6) + let id = mat4() + for r in 0 .. 3: + for c in 0 .. 3: + check abs(product[r, c] - id[r, c]) < 1e-4f + + test "mat4 * vec3": + let m = translate(vec3(10, 20, 30)) + let v = vec3(1.25, -2.5, 3.75) + let result = m * v + check result ~= vec3(11.25, 17.5, 33.75) + + test "mat4 * vec4": + let m = translate(vec3(10, 20, 30)) + let v = vec4(1.25, -2.5, 3.75, 1.0) + let result = m * v + check result ~= vec4(11.25, 17.5, 33.75, 1.0) + +suite "mat4 constructors": + test "scale matrix": + let s = scale(vec3(2, 3, 4)) + check s[0, 0] == 2.0f + check s[1, 1] == 3.0f + check s[2, 2] == 4.0f + check s[3, 3] == 1.0f + check s * vec3(1, 1, 1) ~= vec3(2, 3, 4) + + test "translate matrix": + let t = translate(vec3(10, 20, 30)) + check t[3, 0] == 10.0f + check t[3, 1] == 20.0f + check t[3, 2] == 30.0f + check t * vec3(0, 0, 0) ~= vec3(10, 20, 30) + + test "rotateX matrix": + let r = rotateX(90.0f.toRadians) + # Rotating Y-axis around X by 90 should give Z-axis + check r * vec3(0, 1, 0) ~= vec3(0, 0, 1) + + test "rotateY matrix": + let r = rotateY(90.0f.toRadians) + # Rotating X-axis around Y by 90 should give -Z-axis + check r * vec3(1, 0, 0) ~= vec3(0, 0, -1) + + test "rotateZ matrix": + let r = rotateZ(90.0f.toRadians) + # Rotating X-axis around Z by 90 should give Y-axis + check r * vec3(1, 0, 0) ~= vec3(0, 1, 0) + + test "transform composition order": + # T * R * S: scale first, then rotate, then translate + let t = translate(vec3(10, 20, 30)) + let r = rotateZ(71.0f.toRadians) * rotateY(-23.0f.toRadians) * rotateX(37.0f.toRadians) + let s = scale(vec3(2, 3, 4)) + let transform = t * r * s + check transform[3, 0] ~= 10.0f + check transform[3, 1] ~= 20.0f + check transform[3, 2] ~= 30.0f + + test "3D and 2D composition consistency": + let mat2d = translate(vec2(10, 20)) * rotate(45.toRadians) * scale(vec2(2)) + let mat3d = translate(vec3(10, 20, 0)) * rotateZ(45.toRadians) * scale(vec3(2)) + # The 2D and 3D transforms should produce matching XY results + let v2 = mat2d * vec2(1, 0) + let v3 = mat3d * vec3(1, 0, 0) + check v2.x ~= v3.x + check v2.y ~= v3.y + +suite "vector operations": + test "length": + check vec2(3, 4).length ~= 5.0f + check vec3(1, 2, 2).length ~= 3.0f + check vec4(1, 0, 0, 0).length ~= 1.0f + check dvec3(0, 0, 0).length ~= 0.0 + + test "lengthSq": + check vec2(3, 4).lengthSq ~= 25.0f + check vec3(1, 2, 2).lengthSq ~= 9.0f + check vec4(2, 0, 0, 0).lengthSq ~= 4.0f + + test "normalize": + check normalize(vec2(10, 0)) ~= vec2(1, 0) + check normalize(vec3(0, 0, 5)) ~= vec3(0, 0, 1) + check abs(normalize(vec4(1, 1, 1, 1)).length - 1.0f) < 1e-5f + + test "dist and distSq": + check dist(vec2(0, 0), vec2(3, 4)) ~= 5.0f + check distSq(vec2(0, 0), vec2(3, 4)) ~= 25.0f + check dist(vec3(0, 0, 0), vec3(1, 2, 2)) ~= 3.0f + check distSq(vec3(0, 0, 0), vec3(1, 2, 2)) ~= 9.0f + + test "dot product": + check dot(vec2(1, 0), vec2(0, 1)) ~= 0.0f + check dot(vec2(1, 0), vec2(1, 0)) ~= 1.0f + check dot(vec3(1, 2, 3), vec3(4, 5, 6)) ~= 32.0f + check dot(vec4(1, 2, 3, 4), vec4(5, 6, 7, 8)) ~= 70.0f + + test "dir (point to point)": + let d = dir(vec3(0, 0, 0), vec3(10, 0, 0)) + check abs(d.length - 1.0f) < 1e-5f + let d2 = dir(vec2(0, 0), vec2(0, 5)) + check abs(d2.length - 1.0f) < 1e-5f + + test "dir (angle to vec2)": + check dir(0.0f) ~= vec2(1, 0) + check dir(float32(PI / 2)) ~= vec2(0, 1) + check dir(float32(PI)) ~= vec2(-1, 0) + + test "angle of vec2": + check angle(vec2(1, 0)) ~= 0.0f + check angle(vec2(0, 1)) ~= float32(PI / 2) + check angle(vec2(-1, 0)) ~= float32(PI) + + test "mix (vector lerp)": + check mix(vec2(0, 0), vec2(10, 20), 0.5f) ~= vec2(5, 10) + check mix(vec3(0, 0, 0), vec3(10, 20, 30), 0.25f) ~= vec3(2.5, 5.0, 7.5) + check mix(vec4(0, 0, 0, 0), vec4(4, 8, 12, 16), 0.5f) ~= vec4(2, 4, 6, 8) + + test "mix (per-component vector)": + check mix(vec2(0, 0), vec2(10, 20), vec2(0.5, 1.0)) ~= vec2(5, 20) + check mix(vec3(0, 0, 0), vec3(10, 20, 30), vec3(0.0, 0.5, 1.0)) ~= vec3(0, 10, 30) + + test "clamp (vector bounds)": + check clamp(vec2(5, -5), vec2(0, 0), vec2(3, 3)) == vec2(3, 0) + check clamp(vec3(5, -5, 1), vec3(0, 0, 0), vec3(3, 3, 3)) == vec3(3, 0, 1) + + test "clamp (scalar bounds)": + check clamp(vec2(5, -5), 0.0f, 3.0f) == vec2(3, 0) + check clamp(vec3(5, -5, 1), 0.0f, 3.0f) == vec3(3, 0, 1) + + test "inversesqrt": + check inversesqrt(4.0f) ~= 0.5f + check inversesqrt(1.0f) ~= 1.0f + check inversesqrt(16.0) ~= 0.25 + + test "zmod": + # GLSL-style mod: a - b * floor(a/b) + check zmod(5.5f, 3.0f) ~= 2.5f + check zmod(-1.0f, 3.0f) ~= 2.0f # differs from Nim mod for negatives + check zmod(7.0f, 3.0f) ~= 1.0f + + test "turnAngle": + # Should step toward target angle, clamped by speed + let a = 0.0f + let b = 1.0f + check turnAngle(a, b, 0.5f) ~= 0.5f # step partway + check turnAngle(a, b, 2.0f) ~= b # speed exceeds gap, snap to target + check turnAngle(a, b, 0.01f) ~= 0.01f # small step + +suite "mat4 direction accessors": + test "identity directions": + let m = mat4() + check m.forward ~= vec3(0, 0, 1) + check m.back ~= vec3(0, 0, -1) + check m.left ~= vec3(-1, 0, 0) + check m.right ~= vec3(1, 0, 0) + check m.up ~= vec3(0, 1, 0) + check m.down ~= vec3(0, -1, 0) + + test "rotated directions": + let m = rotateY(float32(PI / 2)) + # After 90° CCW around Y: forward (+Z) rotates toward +X + check m.forward ~= vec3(1, 0, 0) + check m.right ~= vec3(0, 0, -1) + check m.up ~= vec3(0, 1, 0) + + test "rotationOnly strips translation": + let m = translate(vec3(10, 20, 30)) * rotateX(float32(PI / 4)) + let r = rotationOnly(m) + check r.pos ~= vec3(0, 0, 0) + check r.forward ~= m.forward + check r.up ~= m.up + +suite "frustum and projection": + test "frustum matrix": + let f = frustum[float32](-1, 1, -1, 1, 1, 100) + # Near plane, center should map to origin + let v = f * vec4(0, 0, -1, 1) + check v.x ~= 0.0f + check v.y ~= 0.0f + # Should match perspective with 90° fov, aspect 1 + let p = perspective[float32](90, 1, 1, 100) + check f ~= p + +suite "euler angles (extended)": + test "toAngles from origin to target": + let angles = toAngles(vec3(0, 0, 0), vec3(0, 0, 1)) + check angles ~= vec3(0, 0, 0) # looking forward + let angles2 = toAngles(vec3(0, 0, 0), vec3(1, 0, 0)) + check angles2 ~= toAngles(vec3(1, 0, 0)) + + test "toAngles from quaternion": + # Identity quaternion = no rotation + check toAngles(quat(0, 0, 0, 1)) ~= vec3(0, 0, 0) + # Quaternion euler roundtrip + let q = quatRotateX(0.3f) * quatRotateY(0.5f) * quatRotateZ(0.1f) + let anglesFromQuat = toAngles(q) + let anglesFromMat = toAngles(q.mat4()) + check anglesFromQuat ~= anglesFromMat + + test "rotate around arbitrary axis": + let axis = normalize(vec3(1.0, 1.0, 0.0)) + let m = rotate(float32(PI / 2), axis) + # Should be a valid rotation matrix (det = 1, orthogonal) + check abs(determinant(m) - 1.0f) < 0.01f + let inv = inverse(m) + check m * inv ~= mat4() + +suite "quaternion nlerp": + test "nlerp endpoints": let - _ = mat2() - _ = mat3() - _ = mat4() + qx = quatRotateX(0.37f) + qz = quatRotateZ(1.24f) + check nlerp(qx, qz, 0.0f) ~= qx - block: + test "nlerp produces unit quaternions": let - _ = mat2( - 1, 0, - 0, 1 - ) - _ = mat3( - 1, 0, 0, - 0, 1, 0, - 0, 0, 1 - ) - _ = mat4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ) + qx = quatRotateX(0.37f) + qz = quatRotateZ(1.24f) + for i in 0 .. 10: + let q = nlerp(qx, qz, i.float32 / 10.0f) + check abs(q.length - 1.0f) < 1e-5f - block: - # test $ string functions - doAssert $mat2( - 1, 3, - 0, 1 - ) == """mat2( - 1.0, 3.0, - 0.0, 1.0 -)""" - doAssert $mat3( - 1, 3, 0, - 0, 1, 0, - 0, 3, 1 - ) == """mat3( - 1.0, 3.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 3.0, 1.0 -)""" - doAssert $mat4( - 1, 3, 0, 0, + test "nlerp vs slerp similar for close quats": + let + qx = quatRotateX(0.1f) + qy = quatRotateX(0.2f) + nl = nlerp(qx, qy, 0.5f) + sl = slerp(qx, qy, 0.5f) + # For close quaternions, nlerp and slerp should be very similar + check dist(nl, sl) < 0.01f + +suite "orthogonal vector": + test "orthogonal is perpendicular to abs(v)": + # orthogonal() uses abs(v) internally, so result is perpendicular to abs(v) + for v in [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1), + vec3(1, 1, 0), vec3(1, 1, 1), vec3(3, 2, 7)]: + let o = orthogonal(v) + check abs(dot(v, o)) < 1e-5f + + test "orthogonal is nonzero for nonzero input": + for v in [vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1), vec3(5, 3, 2)]: + check orthogonal(v).length > 0.0f + +suite "degenerate matrices": + test "all-zero mat2": + let z = mat2(0, 0, 0, 0) + check determinant(z) == 0.0f + # zero * anything = zero + check z * mat2() ~= z + check mat2() * z ~= z + check z * z ~= z + # zero * vec = zero + check z * vec2(1, 2) ~= vec2(0, 0) + # transpose of zero is zero + check transpose(z) ~= z + + test "all-zero mat3": + let z = mat3(0, 0, 0, 0, 0, 0, 0, 0, 0) + check determinant(z) == 0.0f + check z * mat3() ~= z + check mat3() * z ~= z + check z * z ~= z + check z * vec3(1, 2, 3) ~= vec3(0, 0, 0) + check transpose(z) ~= z + + test "all-zero mat4": + let z = mat4(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + check determinant(z) == 0.0f + check z * mat4() ~= z + check mat4() * z ~= z + check z * z ~= z + check z * vec3(1, 2, 3) ~= vec3(0, 0, 0) + check transpose(z) ~= z + + test "singular mat2 (det=0)": + # Rows are linearly dependent + let s = mat2(1, 2, 2, 4) + check determinant(s) == 0.0f + # Inverse produces Inf/NaN (division by zero det) + let inv = inverse(s) + check vmath.isNan(inv[0, 0]) or abs(inv[0, 0]) == Inf + + test "singular mat3 (det=0)": + # Row 2 = Row 0 + Row 1 + let s = mat3(1, 0, 0, 0, 1, 0, 1, 1, 0) + check abs(determinant(s)) < 1e-6f + let inv = inverse(s) + check vmath.isNan(inv[0, 0]) or abs(inv[0, 0]) == Inf + + test "singular mat4 (det=0)": + # Duplicate rows → det = 0 + let s = mat4( + 1, 0, 0, 0, 0, 1, 0, 0, - 0, 3, 1, 0, - 0, 3, 0, 1 - ) == """mat4( - 1.0, 3.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 3.0, 1.0, 0.0, - 0.0, 3.0, 0.0, 1.0 -)""" - doAssert $dmat2( - 1, 0, - 4, 1 - ) == """dmat2( - 1.0, 0.0, - 4.0, 1.0 -)""" - doAssert $dmat3( - 1, 0, 0, - 4, 1, 0, - 4, 0, 1 - ) == """dmat3( - 1.0, 0.0, 0.0, - 4.0, 1.0, 0.0, - 4.0, 0.0, 1.0 -)""" - doAssert $dmat4( 1, 0, 0, 0, - 4, 1, 0, 0, - 4, 0, 1, 0, - 4, 0, 0, 1 - ) == """dmat4( - 1.0, 0.0, 0.0, 0.0, - 4.0, 1.0, 0.0, 0.0, - 4.0, 0.0, 1.0, 0.0, - 4.0, 0.0, 0.0, 1.0 -)""" - - block: + 0, 0, 0, 1 + ) + check abs(determinant(s)) < 1e-6f + let inv = inverse(s) + check vmath.isNan(inv[0, 0]) or abs(inv[0, 0]) == Inf + + test "zero diagonal mat2": + let m = mat2(0, 3, 7, 0) + check determinant(m) == -(7.0f * 3.0f) # -21 + # Still invertible (det != 0) + let inv = inverse(m) + check m * inv ~= mat2() + + test "zero diagonal mat3": + let m = mat3(0, 1, 0, 0, 0, 1, 1, 0, 0) + # Permutation matrix, det = 1 + check abs(determinant(m) - 1.0f) < 1e-5f + let inv = inverse(m) + check m * inv ~= mat3() + + test "zero diagonal mat4": + # Anti-diagonal matrix + let m = mat4( + 0, 0, 0, 1, + 0, 0, 1, 0, + 0, 1, 0, 0, + 1, 0, 0, 0 + ) + check abs(determinant(m)) > 0.0f + let inv = inverse(m) + check m * inv ~= mat4() + + test "near-singular mat3 (tiny det)": + let m = dmat3( + 1.0, 0.0, 0.0, + 0.0, 1e-15, 0.0, + 0.0, 0.0, 1.0 + ) + check abs(determinant(m)) < 1e-10 + check determinant(m) != 0.0 + # Inverse exists but is huge + let inv = inverse(m) + check inv[1, 1] > 1e14 + + test "scale by zero (rank-deficient mat4)": + let m = scale(vec3(1, 0, 1)) + check determinant(m) == 0.0f + # Multiply still works, just collapses one axis + check m * vec3(5, 5, 5) ~= vec3(5, 0, 5) + + test "all-ones matrices": + let m2 = mat2(1, 1, 1, 1) + check determinant(m2) == 0.0f + # m * m = 2 * m (idempotent up to scaling) + check m2 * m2 ~= mat2(2, 2, 2, 2) + + let m3 = mat3(1, 1, 1, 1, 1, 1, 1, 1, 1) + check determinant(m3) == 0.0f + check m3 * m3 ~= mat3(3, 3, 3, 3, 3, 3, 3, 3, 3) + + test "negative identity": + let m2 = mat2(-1, 0, 0, -1) + check determinant(m2) == 1.0f + check m2 * m2 ~= mat2() # (-I)^2 = I + check inverse(m2) ~= m2 # (-I)^-1 = -I + + let m3 = mat3(-1, 0, 0, 0, -1, 0, 0, 0, -1) + check determinant(m3) == -1.0f + check m3 * m3 ~= mat3() + check inverse(m3) ~= m3 + + let m4 = mat4( + -1, 0, 0, 0, + 0,-1, 0, 0, + 0, 0,-1, 0, + 0, 0, 0, 1 + ) + check abs(determinant(m4) - (-1.0f)) < 1e-5f + check inverse(m4) ~= m4 + + test "projection matrix (non-invertible rank 1)": + # Projects onto x-axis: P^2 = P + let p = mat3(1, 0, 0, 0, 0, 0, 0, 0, 0) + check determinant(p) == 0.0f + check p * p ~= p # idempotent + check p * vec3(5, 7, 9) ~= vec3(5, 0, 0) + + test "very large values": + let big = 1e18f + let m = mat2(big, 0, 0, big) + check abs(determinant(m) - big * big) < 1e30f + let inv = inverse(m) + check inv[0, 0] ~= (1.0f / big) + check m * inv ~= mat2() + + test "mixed large and small values": + let m = dmat3( + 1e10, 0.0, 0.0, + 0.0, 1e-10, 0.0, + 0.0, 0.0, 1.0 + ) + let inv = inverse(m) + check m * inv ~= dmat3() + + test "skew/shear matrix": + # Shear in x by y + let m = mat3(1, 0, 0, 3, 1, 0, 0, 0, 1) + check determinant(m) == 1.0f + let inv = inverse(m) + check m * inv ~= mat3() + # Shearing (1, 0) should give (1, 0), shearing (0, 1) should give (3, 1) + check m * vec3(0, 1, 0) ~= vec3(3, 1, 0) + +suite "quaternion constructors": + test "identity quaternion": + let q = quat(0, 0, 0, 1) + check q.x == 0.0f + check q.y == 0.0f + check q.z == 0.0f + check q.w == 1.0f + + test "axis-angle constructors": + let qx = quatRotateX(37.0f.toRadians) + let qy = quatRotateY(-23.0f.toRadians) + let qz = quatRotateZ(71.0f.toRadians) + # Should be unit quaternions + check abs(qx.length - 1.0f) < 1e-5f + check abs(qy.length - 1.0f) < 1e-5f + check abs(qz.length - 1.0f) < 1e-5f + + test "fromAxisAngle": + let axis = normalize(vec3(1.0, 2.0, -3.0)) + let angle = 48.0f.toRadians + let q = fromAxisAngle(axis, angle) + check abs(q.length - 1.0f) < 1e-5f + check q.w ~= cos(angle * 0.5f) + + test "quaternion matrix constructors match rotation matrices": + check quatRotateX(PI / 2).mat4() ~= rotateX(PI / 2) + check quatRotateY(PI / 2).mat4() ~= rotateY(PI / 2) + check quatRotateZ(PI / 2).mat4() ~= rotateZ(PI / 2) + +suite "quaternion arithmetic": + test "quat + - * / operators": let - _ = mat2( - vec2(1, 0), - vec2(0, 1) - ) - _ = mat3( - vec3(1, 0, 0), - vec3(0, 1, 0), - vec3(0, 0, 1) - ) - _ = mat4( - vec4(1, 0, 0, 0), - vec4(0, 1, 0, 0), - vec4(0, 0, 1, 0), - vec4(0, 0, 0, 1) + a = dquat(1, 2, 3, 4) + b = dquat(-0.5, 0.25, 2.0, -3.0) + check a + b ~= dquat(0.5, 2.25, 5.0, 1.0) + check a - b ~= dquat(1.5, 1.75, 1.0, 7.0) + check a * 2.0 ~= dquat(2, 4, 6, 8) + check b / 2.0 ~= dquat(-0.25, 0.125, 1.0, -1.5) + +suite "quaternion multiply": + test "quaternion composition matches matrix composition": + let + qx = quatRotateX(0.37) + qy = quatRotateY(-0.91) + qz = quatRotateZ(1.24) + mxyz = rotateX(0.37) * rotateY(-0.91) * rotateZ(1.24) + check quatMultiply(quatMultiply(qx, qy), qz).mat4() ~= mxyz + + test "quaternion multiply identity": + let q = fromAxisAngle(normalize(vec3(1, 2, -3)), 48.0f.toRadians) + let identity = quat(0, 0, 0, 1) + check quatMultiply(q, identity) ~= q + check quatMultiply(identity, q) ~= q + +suite "quaternion vector rotation": + test "rotate basis vectors": + let x = dvec3(1, 0, 0) + let y = dvec3(0, 1, 0) + check quatRotateY(PI / 2) * x ~= dvec3(0, 0, -1) + check quatRotateX(PI / 2) * y ~= dvec3(0, 0, 1) + check quatRotateZ(PI / 2) * x ~= dvec3(0, 1, 0) + + test "quatRotate matches matrix multiply": + for _ in 0 ..< 1000: + let + axis = dvec3(rand(2.0)-1.0, rand(2.0)-1.0, rand(2.0)-1.0) + angle = rand(-PI .. PI) + q = fromAxisAngle(axis.normalize, angle) + v = dvec3(rand(2.0)-1.0, rand(2.0)-1.0, rand(2.0)-1.0) + check quatRotate(q, v) ~= q.mat4() * v + check q * v ~= q.mat4() * v + +suite "quaternion matrix roundtrip": + test "identity roundtrip": + let m = mat4() + check m.quat().mat4() ~= m + + test "single axis rotations": + for angle in [PI/6, PI/4, PI/3, PI/2, PI, -PI/4]: + let mx = rotateX(angle) + check mx.quat().mat4() ~= mx + let my = rotateY(angle) + check my.quat().mat4() ~= my + let mz = rotateZ(angle) + check mz.quat().mat4() ~= mz + + test "arbitrary rotations fuzz": + for _ in 0 ..< 2000: + let m = rotate( + PI*rand(2.0), + dvec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() ) + check m.quat().mat4() ~= m - block: + test "hard decomposition near 180 degrees": let - _ = dmat2() - _ = dmat3() - _ = dmat4() - - block: + axis = normalize(vec3(1.0, -2.0, 3.0)) + angle = 170.0f.toRadians + q = fromAxisAngle(axis, angle) + m = q.mat4() + check m.quat().mat4() ~= m + +suite "quaternion inverse": + test "q * q^-1 = identity": + let q = fromAxisAngle(normalize(dvec3(1, 2, -3)), 48.0.toRadians) + let product = quatMultiply(q, quatInverse(q)) + check abs(product.x) < 1e-5 + check abs(product.y) < 1e-5 + check abs(product.z) < 1e-5 + check abs(product.w - 1.0) < 1e-5 + + test "inverse undoes rotation": let - _ = dmat2( - 1, 0, - 0, 1 - ) - _ = dmat3( - 1, 0, 0, - 0, 1, 0, - 0, 0, 1 - ) - _ = dmat4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1 - ) - - block: + q = fromAxisAngle(normalize(dvec3(1, 2, -3)), 48.0.toRadians) + v = dvec3(1.25, -2.5, 3.75) + rotated = quatRotate(q, v) + unrotated = quatRotate(quatInverse(q), rotated) + check unrotated ~= v + + test "unit quaternion inverse = conjugate": + let q = fromAxisAngle(normalize(dvec3(1, 2, -3)), 48.0.toRadians) + let inv = quatInverse(q) + let conj = dquat(-q.x, -q.y, -q.z, q.w) + check inv ~= conj + + test "inverse fuzz": + for _ in 0 ..< 1000: + let + axis = dvec3(rand(2.0)-1.0, rand(2.0)-1.0, rand(2.0)-1.0).normalize() + q = fromAxisAngle(axis, rand(-PI .. PI)) + product = quatMultiply(q, quatInverse(q)) + check abs(product.w) ~= 1.0 + +suite "quaternion to axis-angle": + test "known axis-angle roundtrip": let - _ = dmat2( - dvec2(1, 0), - dvec2(0, 1) - ) - _ = dmat3( - dvec3(1, 0, 0), - dvec3(0, 1, 0), - dvec3(0, 0, 1) - ) - _ = dmat4( - dvec4(1, 0, 0, 0), - dvec4(0, 1, 0, 0), - dvec4(0, 0, 1, 0), - dvec4(0, 0, 0, 1) - ) - - block: - var - d2 = dmat2() - d3 = dmat3() - d4 = dmat4() + axis = normalize(dvec3(1, 2, -3)) + angle = 48.0.toRadians + q = fromAxisAngle(axis, angle) + (extractedAxis, extractedAngle) = toAxisAngle(q) + check extractedAxis ~= axis + check abs(extractedAngle - angle) < 1e-5 + + test "identity gives zero angle": + let (_, angle) = toAxisAngle(dquat(0, 0, 0, 1)) + check abs(angle) < 1e-5 + + test "90 degree axes": + for axisVec in [dvec3(1, 0, 0), dvec3(0, 1, 0), dvec3(0, 0, 1)]: + let + q = fromAxisAngle(axisVec, PI / 2) + (a, ang) = toAxisAngle(q) + check a ~= axisVec + check abs(ang - PI / 2) < 1e-5 + + test "axis-angle fuzz": + for _ in 0 ..< 1000: + let + axis = dvec3(rand(2.0)-1.0, rand(2.0)-1.0, rand(2.0)-1.0).normalize() + angle = rand(0.001 .. PI) + q = fromAxisAngle(axis, angle) + (a, ang) = toAxisAngle(q) + check a ~= axis + check abs(ang - angle) < 1e-4 + +suite "slerp": + test "endpoints": + let + qx = quatRotateX(0.37) + qz = quatRotateZ(1.24) + check slerp(qx, qz, 0.0) ~= qx - d2[0, 0] = 123.123 - d2[1, 1] = 123.123 + test "midpoint equidistant": + let + qx = quatRotateX(0.37) + qz = quatRotateZ(1.24) + mid = slerp(qx, qz, 0.5) + a0 = arccos(clamp(dot(qx, mid), -1.0, 1.0)) + a1 = arccos(clamp(dot(mid, qz), -1.0, 1.0)) + check abs(a0 - a1) < 1e-5 + + test "slerp unit length": + let + qx = quatRotateX(0.37) + qz = quatRotateZ(1.24) + for i in 0 .. 10: + let q = slerp(qx, qz, i.float64 / 10.0) + check abs(q.length - 1.0) < 1e-5 - d3[0, 0] = 123.123 - d3[1, 1] = 123.123 - d3[2, 2] = 123.123 + test "slerp identical inputs": + let qx = quatRotateX(0.37) + check slerp(qx, qx, 0.5) ~= qx - d4[0, 0] = 123.123 - d4[1, 1] = 123.123 - d4[2, 2] = 123.123 - d4[3, 3] = 123.123 - -block: - # Test basic mat functions. - doAssert dmat3().transpose() ~= dmat3( - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 0.0, 0.0, 1.0 - ) - doAssert dmat4().transpose() ~= dmat4( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ) - - doAssert scale(dvec2(1, 2)) ~= dmat3( - 1.0, 0.0, 0.0, - 0.0, 2.0, 0.0, - 0.0, 0.0, 1.0 - ) - doAssert scale(dvec3(2, 2, 3)) ~= dmat4( - 2.0, 0.0, 0.0, 0.0, - 0.0, 2.0, 0.0, 0.0, - 0.0, 0.0, 3.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ) - - doAssert translate(dvec2(1, 2)) ~= dmat3( - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 1.0, 2.0, 1.0 - ) - doAssert translate(dvec3(1, 2, 3)) ~= dmat4( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 1.0, 2.0, 3.0, 1.0 - ) - - doAssert rotate(1.0) ~= dmat3( - 0.5403023058681398, -0.8414709848078965, 0.0, - 0.8414709848078965, 0.5403023058681398, 0.0, - 0.0, 0.0, 1.0 - ) - - doAssert scale(dvec2(2)) ~= dmat3( - 2.0, 0.0, 0.0, - 0.0, 2.0, 0.0, - 0.0, 0.0, 1.0 - ) - doAssert scale(dvec3(2)) ~= dmat4( - 2.0, 0.0, 0.0, 0.0, - 0.0, 2.0, 0.0, 0.0, - 0.0, 0.0, 2.0, 0.0, - 0.0, 0.0, 0.0, 1.0 - ) - - doAssert translate(dvec2(2)) ~= dmat3( - 1.0, 0.0, 0.0, - 0.0, 1.0, 0.0, - 2.0, 2.0, 1.0 - ) - doAssert translate(dvec3(2)) ~= dmat4( - 1.0, 0.0, 0.0, 0.0, - 0.0, 1.0, 0.0, 0.0, - 0.0, 0.0, 1.0, 0.0, - 2.0, 2.0, 2.0, 1.0 - ) - - doAssert rotate(1.0).inverse() ~= dmat3( - 0.5403023058681398, 0.8414709848078965, -0.0, - -0.8414709848078965, 0.5403023058681398, -0.0, - 0.0, -0.0, 1.0 - ) - doAssert rotate(1.0, dvec3(1, 0, 0)).inverse() ~= dmat4( - 1.0, 0.0, 0.0, 0.0, - 0.0, 0.5403023058681398, 0.8414709848078965, 0.0, - 0.0, -0.8414709848078965, 0.5403023058681398, -0.0, - 0.0, 0.0, 0.0, 1.0 - ) - - - block: - doAssert translate(vec2(1, 2)).pos == vec2(1, 2) - - var translation = translate(vec2(1, 2)) - translation.pos = vec2(3, 4) - doAssert translation.pos == vec2(3, 4) - - block: - doAssert translate(vec3(1, 2, 3)).pos == vec3(1, 2, 3) - - var translation = translate(vec3(1, 2, 3)) - translation.pos = vec3(3, 4, 5) - doAssert translation.pos == vec3(3, 4, 5) - -block: - # Test basic vector mat4 and quat. - var m1 = mat4( - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m2 ~= mat4( - 1.00000, 0.00000, 0.00000, 0.00000, - 0.00000, 1.00000, 0.00000, 0.00000, - 0.00000, 0.00000, 1.00000, 0.00000, - 0.00000, 0.00000, 0.00000, 1.00000 - ) - doAssert m1 ~= (m2) - -block: - # Test basic vector mat4 -1. - var m1 = mat4( - 1, 0, 0, 0, - 0, 0, -1, 0, - 0, 1, 0, 0, - 0, 0, 0, 1) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test Y 90. - var m1 = rotate(PI/2, dvec3(0, 1, 0)) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test -Y 90. - var m1 = rotate(PI/2, dvec3(0, -1, 0)) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test X 90. - var m1 = rotate(PI/2, dvec3(1, 0, 0)) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test Y 90. - var m1 = rotate(PI/2, dvec3(1, 0, 0)) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test 1,1,1 1.11rad. - var m1 = rotate(PI*1.11, dvec3(1, 1, 1).normalize()) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test 1,1,1 1.11rad. - var m1 = rotate(PI*1.11, dvec3(-1, 1, 1).normalize()) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test 1,1,1 1.11rad. - var m1 = rotate(PI*1.11, dvec3(-1, 0.34, 1.123).normalize()) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test super random quat test. - for i in 0 ..< 1000: - var m1 = rotate( - PI*rand(2.0), - dvec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() - ) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 - -block: - # Test *=1 /=1 don't change anything. - var v2 = vec2(0, 0) - v2 *= 1 - v2 /= 1 - doAssert v2 == vec2(0, 0) - - var v3 = vec3(0, 0, 0) - v3 *= 1 - v3 /= 1 - doAssert v3 == vec3(0, 0, 0) - - var v4 = vec4(0, 0, 0, 0) - v4 *= 1 - v4 /= 1 - doAssert v4 == vec4(0, 0, 0, 0) - - var q = quat(0, 0, 0, 0) - q *= 1 - q /= 1 - doAssert q == quat(0, 0, 0, 0) - -block: - # Test matrix and vector multiplication. - var a3 = mat3( - 0.9659258723258972, -0.258819043636322, 0.0, - 0.258819043636322, 0.9659258723258972, 0.0, - -25.00000953674316, 70.09619140625, 1.0 - ) - var b3 = mat3( - 0.9659258127212524, 0.258819043636322, 0.0, - -0.258819043636322, 0.9659258127212524, 0.0, - 77.64571380615234, 0.0, 1.0 - ) - - when not defined(js): - # TODO: Figure out why we loose soo much precision in js. - - doAssert a3 * b3 ~= mat3( - 1.0000, 0.0000, 0.0000, - 0.0000, 1.0000, 0.0000, - 50.0000, 50.0000, 1.0000 + test "slerp with opposite quaternion": + let + qx = quatRotateX(0.37) + qz = quatRotateZ(1.24) + qNeg = -qz + result = slerp(qx, qNeg, 0.5) + check abs(result.length - 1.0) < 1e-5 + + test "slerp fuzz unit length": + for _ in 0 ..< 1000: + let + a = fromAxisAngle(dvec3(rand(2.0)-1, rand(2.0)-1, rand(2.0)-1).normalize(), rand(-PI .. PI)) + b = fromAxisAngle(dvec3(rand(2.0)-1, rand(2.0)-1, rand(2.0)-1).normalize(), rand(-PI .. PI)) + t = rand(0.0 .. 1.0) + q = slerp(a, b, t) + check abs(q.length - 1.0) < 1e-5 + +suite "fromTwoVectors": + test "basic rotation": + let + a = vec3(1, 0, 0) + b = vec3(0, 1, 0) + q = fromTwoVectors(a, b) + check q.mat4() * a ~= b + + test "fromTwoVectors fuzz": + for _ in 0 ..< 1000: + let + a = vec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() + b = vec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() + q = fromTwoVectors(a, b) + check dist(q.mat4() * a, b) < 1e-5 + +suite "cross product": + test "basis vectors": + check cross(vec3(1, 0, 0), vec3(0, 1, 0)) ~= vec3(0, 0, 1) + check cross(vec3(0, 1, 0), vec3(1, 0, 0)) ~= vec3(0, 0, -1) + + test "cross product anticommutative": + let + a = normalize(vec3(1.0, 2.0, 3.0)) + b = normalize(vec3(-1.0, 0.5, 2.0)) + check cross(a, b) ~= -cross(b, a) + +suite "euler angles": + const PI = PI.float32 + + proc eq(a, b: Vec3): bool = + const epsilon = 0.001 + abs(angleBetween(a.x, b.x)) < epsilon and + abs(angleBetween(a.y, b.y)) < epsilon and + abs(angleBetween(a.z, b.z)) < epsilon + + test "from vector": + check vec3(0, 0, 1).toAngles.eq vec3(0f, 0f, 0f) # forward + check vec3(0, 0, -1).toAngles.eq vec3(0f, PI, 0f) # back + check vec3(-1, 0, 0).toAngles.eq vec3(0f, PI/2, 0f) # right + check vec3(1, 0, 0).toAngles.eq vec3(0f, -PI/2, 0f) # left + check vec3(0, 1, 0).toAngles.eq vec3(PI/2, 0f, 0f) # up + check vec3(0, -1, 0).toAngles.eq vec3(-PI/2, 0f, 0f) # down + + test "from matrix": + check mat4().toAngles.eq vec3(0, 0, 0) + check rotateX(10.toRadians()).toAngles.eq vec3(10.toRadians(), 0, 0) + check rotateY(10.toRadians()).toAngles.eq vec3(0, 10.toRadians(), 0) + check rotateZ(10.toRadians()).toAngles.eq vec3(0, 0, 10.toRadians()) + + test "euler roundtrip fuzz (non-polar)": + for _ in 0 .. 1000: + let + xr = rand(-89.9f .. 89.9f).toRadians + yr = rand(-180 .. 180).toRadians + zr = rand(-180 .. 180).toRadians + b = vec3(xr, yr, zr) + a = fromAngles(b).toAngles() + check a.eq(b) + +suite "lookAt": + test "basic lookAt": + let m = lookAt(vec3(5, 5, 5), vec3(0, 0, 0), vec3(0, 1, 0)) + check m[3, 3] ~= 1.0f + # Should be orthogonal (inverse == transpose for the rotation part) + let det = determinant(m) + check abs(det - 1.0f) < 0.01f + + test "lookAt with default up": + let m = lookAt(vec3(0, 0, 5), vec3(0, 0, 0)) + check m * vec3(0, 0, 0) ~= vec3(0, 0, -5) + +suite "projection matrices": + test "ortho": + let o = ortho[float32](-1, 1, 1, -1, -1000, 1000) + check o ~= mat4( + 1.0, 0.0, 0.0, 0.0, + 0.0, -1.0, 0.0, 0.0, + 0.0, 0.0, -0.001000000047497451, 0.0, + -0.0, 0.0, -0.0, 1.0 ) - doAssert a3 * vec2(77.64571380615234, 0) ~= vec2(50.0, 50.0) - - doAssert mat3(1, 2, 3, 4, 5, 6, 7, 8, 9) * - mat3(10, 20, 30, 40, 50, 60, 70, 80, 90) ~= mat3( - 300.0000, 360.0000, 420.0000, - 660.0000, 810.0000, 960.0000, - 1020.0000, 1260.0000, 1500.0000 + test "perspective": + let p = perspective[float32](75, 1.666, 1, 1000) + check p ~= mat4( + 0.7822480201721191, 0.0, 0.0, 0.0, + 0.0, 1.30322527885437, 0.0, 0.0, + 0.0, 0.0, -1.002002000808716, -1.0, + 0.0, 0.0, -2.002002000808716, 0.0 ) -block: - # test quat and matrix - doAssert ortho[float32](-1, 1, 1, -1, -1000, 1000) ~= mat4( - 1.0, 0.0, 0.0, 0.0, - 0.0, -1.0, 0.0, 0.0, - 0.0, 0.0, -0.001000000047497451, - 0.0, -0.0, 0.0, -0.0, 1.0 - ) - - doAssert perspective[float32](75, 1.666, 1, 1000) ~= mat4( - 0.7822480201721191, 0.0, 0.0, 0.0, - 0.0, 1.30322527885437, 0.0, 0.0, - 0.0, 0.0, -1.002002000808716, -1.0, - 0.0, 0.0, -2.002002000808716, 0.0 - ) - - # Test super random quat test. - for i in 0 ..< 1000: - var m1 = rotate( - PI*rand(2.0), - dvec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() - ) - var q1 = m1.quat() - var m2 = q1.mat4() - doAssert m1 ~= m2 +suite "vector constructors": + test "all types compile": + check bvec2(true, false) == bvec2(true, false) + check ivec3(-1, 2, 3) == ivec3(-1, 2, 3) + check uvec4(1, 2, 3, 4) == uvec4(1, 2, 3, 4) + check vec2(1.0) == vec2(1.0, 1.0) + check dvec3(1.0) == dvec3(1.0, 1.0, 1.0) + + test "composite constructors": + check vec3(vec2(1, 2), 3) == vec3(1, 2, 3) + check vec4(vec3(1, 2, 3), 4) == vec4(1, 2, 3, 4) + check vec4(vec2(1, 2), vec2(3, 4)) == vec4(1, 2, 3, 4) + + test "type conversions": + check vec2(ivec2(1, 1)) == vec2(1, 1) + check vec2(uvec2(5, 5)) == vec2(5, 5) + check vec3(ivec3(1, 2, 3)) == vec3(1, 2, 3) + check vec4(uvec4(17, 18, 19, 20)) == vec4(17, 18, 19, 20) + +suite "vector arithmetic": + test "vec2 operations": + let a = vec2(1, 2) + let b = vec2(7, 6) + check a + b ~= vec2(8, 8) + check a - b ~= vec2(-6, -4) + check a * 2.0 ~= vec2(2, 4) + check a / 2.0 ~= vec2(0.5, 1.0) + + test "vec3 operations": + let a = vec3(1, 2, 3) + let b = vec3(7, 6, 5) + check a + b ~= vec3(8, 8, 8) + check a - b ~= vec3(-6, -4, -2) + + test "vec4 operations": + let a = vec4(1, 2, 3, 4) + let b = vec4(7, 6, 5, 4) + check a + b ~= vec4(8, 8, 8, 8) + check a - b ~= vec4(-6, -4, -2, 0) + + test "compound assignment": + var a = vec3(1, 2, 3) + a += vec3(7, 6, 5) + check a ~= vec3(8, 8, 8) + a -= vec3(7, 6, 5) + check a ~= vec3(1, 2, 3) + + test "min max": + check min(vec3(10, -10, 7), vec3(-10, 10, 0)) == vec3(-10, -10, 0) + check max(vec3(10, -10, 7), vec3(-10, 10, 0)) == vec3(10, 10, 7) + + test "equality and inequality": + check vec2(1, 1) == vec2(1, 1) + check vec2(1, 1) != vec2(1, 2) + check vec3(1, 1, 1) == vec3(1, 1, 1) + check vec3(1, 1, 1) != vec3(1, 1, 2) + check vec4(1, 1, 1, 1) == vec4(1, 1, 1, 1) + check vec4(1, 1, 1, 1) != vec4(1, 1, 1, 2) + +suite "vector swizzling": + test "vec2 swizzle read": + let a = vec2(1, 2) + check a.x == 1.0 + check a.y == 2.0 + check a.yx == vec2(2, 1) + check a.xxx == vec3(1, 1, 1) + + test "vec2 swizzle write": + var a = vec2(1, 2) + a.yx = vec2(-1, -2) + check a == vec2(-2, -1) + + test "vec4 swizzle self-assignment": + var b = vec4(1, 2, 3, 4) + b.wzyx = b + check b == vec4(4, 3, 2, 1) + +suite "string representation": + test "vec2 $": + check $vec2(1.0, 2.0) == "vec2(1.0, 2.0)" + + test "vec3 $": + check $vec3(1.0, 2.0, 3.0) == "vec3(1.0, 2.0, 3.0)" + + test "vec4 $": + check $vec4(1.0, 2.0, 3.0, 4.0) == "vec4(1.0, 2.0, 3.0, 4.0)" + + test "dvec2 $": + check $dvec2(1.0, 2.0) == "dvec2(1.0, 2.0)" + + test "dvec3 $": + check $dvec3(1.0, 2.0, 3.0) == "dvec3(1.0, 2.0, 3.0)" + + test "dvec4 $": + check $dvec4(1.0, 2.0, 3.0, 4.0) == "dvec4(1.0, 2.0, 3.0, 4.0)" + + test "ivec2 $": + check $ivec2(1, 2) == "ivec2(1, 2)" + + test "ivec3 $": + check $ivec3(1, 2, 3) == "ivec3(1, 2, 3)" + + test "ivec4 $": + check $ivec4(1, 2, 3, 4) == "ivec4(1, 2, 3, 4)" + + test "uvec2 $": + check $uvec2(1, 2) == "uvec2(1, 2)" + + test "uvec3 $": + check $uvec3(1, 2, 3) == "uvec3(1, 2, 3)" + + test "uvec4 $": + check $uvec4(1, 2, 3, 4) == "uvec4(1, 2, 3, 4)" + + test "bvec2 $": + check $bvec2(true, false) == "bvec2(true, false)" + + test "bvec3 $": + check $bvec3(true, false, true) == "bvec3(true, false, true)" + + test "bvec4 $": + check $bvec4(true, false, true, false) == "bvec4(true, false, true, false)" + + test "quat $ (prints as vec4)": + check $quat(1.0, 2.0, 3.0, 4.0) == "vec4(1.0, 2.0, 3.0, 4.0)" + check $dquat(1.0, 2.0, 3.0, 4.0) == "dvec4(1.0, 2.0, 3.0, 4.0)" + + test "mat2 $": + check $mat2(1, 2, 3, 4) == """mat2( + 1.0, 2.0, + 3.0, 4.0 +)""" -block: - # test fromTwoVectors - let - a = vec3(1, 0, 0) - b = vec3(0, 1, 0) - q1 = fromTwoVectors(a, b) - doAssert q1.mat4 * a ~= b + test "mat3 $": + check $mat3(1, 2, 3, 4, 5, 6, 7, 8, 9) == """mat3( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 +)""" - for i in 0 ..< 1000: - let - a = vec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() - b = vec3(rand(2.0)-0.5, rand(2.0)-0.5, rand(2.0)-0.5).normalize() - q = fromTwoVectors(a, b) - doAssert dist(q.mat4 * a, b) < 1E5 + test "mat4 $": + check $mat4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) == """mat4( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 +)""" -block: - let mat2d = translate(vec2(10, 20)) * rotate(45.toRadians) * scale(vec2(2)) + test "dmat2 $": + check $dmat2(1, 2, 3, 4) == """dmat2( + 1.0, 2.0, + 3.0, 4.0 +)""" - let mat3d = translate(vec3(10, 20, 0)) * rotateZ(45.toRadians) * scale(vec3(2)) + test "dmat3 $": + check $dmat3(1, 2, 3, 4, 5, 6, 7, 8, 9) == """dmat3( + 1.0, 2.0, 3.0, + 4.0, 5.0, 6.0, + 7.0, 8.0, 9.0 +)""" - doAssert mat2d ~= mat3( - 1.414213538169861, -1.414213538169861, 0.0, - 1.414213538169861, 1.414213538169861, 0.0, - 10.0, 20.0, 1.0 - ) + test "dmat4 $": + check $dmat4(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) == """dmat4( + 1.0, 2.0, 3.0, 4.0, + 5.0, 6.0, 7.0, 8.0, + 9.0, 10.0, 11.0, 12.0, + 13.0, 14.0, 15.0, 16.0 +)""" - doAssert mat3d ~= mat4( - 1.414213418960571, -1.41421365737915, 0.0, 0.0, - 1.41421365737915, 1.414213418960571, 0.0, 0.0, - 0.0, 0.0, 2.0, 0.0, - 10.0, 20.0, 0.0, 1.0 - ) + test "mat4 identity $": + check $mat4() == """mat4( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 0.0, 0.0, 0.0, 1.0 +)""" -block: - let - a2 = vec2(10, -10) - b2 = vec2(-10, 10) - a3 = vec3(10, -10, 7) - b3 = vec3(-10, 10, 0) - a4 = vec4(10, -10, 7, -2) - b4 = vec4(-10, 10, 0, -1) - - doAssert min(a2, b2) == vec2(-10, -10) - doAssert min(a3, b3) == vec3(-10, -10, 0) - doAssert min(a4, b4) == vec4(-10, -10, 0, -2) - - doAssert max(a2, b2) == vec2(10, 10) - doAssert max(a3, b3) == vec3(10, 10, 7) - doAssert max(a4, b4) == vec4(10, 10, 7, -1) - - doAssert mix(10f, 7, 0.75) == 7.75 - doAssert mix(a2, b2, 0.75) == vec2(-5.0, 5.0) - doAssert mix(a3, b3, 0.75) == vec3(-5.0, 5.0, 1.75) - doAssert mix(a4, b4, 0.75) == vec4(-5.0, 5.0, 1.75, -1.25) - - doAssert `mod`(1, 2) == 1 - doAssert `mod`(vec2(12, 6), vec2(6, 12)) == vec2(0, 6) - doAssert `mod`(vec3(12, 6, 18), vec3(6, 12, 7)) == vec3(0, 6, 4) - doAssert `mod`(vec4(12, 6, 18, 16), vec4(6, 12, 7, 15)) == vec4(0, 6, 4, 1) - - doAssert `zmod`(1, 2) == 1 - doAssert `zmod`(vec2(12, 6), vec2(6, 12)) == vec2(0, 6) - doAssert `zmod`(vec3(12, 6, 18), vec3(6, 12, 7)) == vec3(0, 6, 4) - doAssert `zmod`(vec4(12, 6, 18, 16), vec4(6, 12, 7, 15)) == vec4(0, 6, 4, 1) - -block: - doAssert vec2(1, 1) == vec2(1, 1) - doAssert dvec2(2, 2) == dvec2(2, 2) - doAssert bvec2(true, true) == bvec2(true, true) - doAssert ivec2(3, 3) == ivec2(3, 3) - doAssert uvec2(3, 3) == uvec2(3, 3) - - doAssert vec3(1, 1, 1) == vec3(1, 1, 1) - doAssert dvec3(2, 2, 2) == dvec3(2, 2, 2) - doAssert bvec3(true, true, true) == bvec3(true, true, true) - doAssert ivec3(3, 3, 3) == ivec3(3, 3, 3) - doAssert uvec3(3, 3, 3) == uvec3(3, 3, 3) - - doAssert vec4(1, 1, 1, 1) == vec4(1, 1, 1, 1) - doAssert dvec4(2, 2, 2, 2) == dvec4(2, 2, 2, 2) - doAssert bvec4(true, true, true, false) == bvec4(true, true, true, false) - doAssert ivec4(3, 3, 3, 3) == ivec4(3, 3, 3, 3) - doAssert uvec4(3, 3, 3, 3) == uvec4(3, 3, 3, 3) - - doAssert vec2(1, 1) != vec2(1, 2) - doAssert dvec2(2, 2) != dvec2(2, 3) - doAssert bvec2(true, true) != bvec2(true, false) - doAssert ivec2(3, 3) != ivec2(3, 4) - doAssert uvec2(3, 3) != uvec2(3, 4) - - doAssert vec3(1, 1, 1) != vec3(1, 1, 2) - doAssert dvec3(2, 2, 2) != dvec3(2, 2, 3) - doAssert bvec3(true, true, true) != bvec3(true, true, false) - doAssert ivec3(3, 3, 3) != ivec3(3, 3, 4) - doAssert uvec3(3, 3, 3) != uvec3(3, 3, 4) - - doAssert vec4(1, 1, 1, 1) != vec4(1, 1, 1, 2) - doAssert dvec4(2, 2, 2, 2) != dvec4(2, 2, 2, 3) - doAssert bvec4(true, true, true, false) != bvec4(true, true, true, true) - doAssert ivec4(3, 3, 3, 3) != ivec4(3, 3, 3, 4) - doAssert uvec4(3, 3, 3, 3) != uvec4(3, 3, 3, 4) - -block: - doAssert vec2(ivec2(1, 1)) == vec2(1, 1) - doAssert vec2(uvec2(5, 5)) == vec2(5, 5) - doAssert ivec2(uvec2(23, 23)) == ivec2(23, 23) - doAssert uvec2(ivec2(12, 12)) == uvec2(12, 12) - doAssert vec3(ivec3(1, 2, 3)) == vec3(1, 2, 3) - doAssert vec3(uvec3(4, 5, 6)) == vec3(4, 5, 6) - doAssert ivec3(uvec3(7, 8, 9)) == ivec3(7, 8, 9) - doAssert uvec3(ivec3(10, 11, 12)) == uvec3(10, 11, 12) - doAssert vec4(ivec4(13, 14, 15, 16)) == vec4(13, 14, 15, 16) - doAssert vec4(uvec4(17, 18, 19, 20)) == vec4(17, 18, 19, 20) - doAssert ivec4(uvec4(21, 22, 23, 24)) == ivec4(21, 22, 23, 24) - doAssert uvec4(ivec4(25, 26, 27, 28)) == uvec4(25, 26, 27, 28) - -block: - # Test for https://github.com/treeform/vmath/issues/44 - doAssert PI.toDegrees() == 180 - doAssert (PI*2).toDegrees() == 360 - -block: - # Test for https://github.com/treeform/vmath/issues/45 - block: - let a = uvec2(10, 10) - var b: UVec2 - when compiles(b = a / 2): doAssert false # type mismatch - b = a div 2 - - block: - let a = vec2(10, 10) - var b: Vec2 - b = a / 2 - when compiles(b = a div 2): doAssert false # type mismatch - -proc eq(a, b: Vec3): bool = - const epsilon = 0.001 - return abs(angleBetween(a.x, b.x)) < epsilon and - abs(angleBetween(a.y, b.y)) < epsilon and - abs(angleBetween(a.z, b.z)) < epsilon - -const PI = PI.float32 - -block: - # test Euler angles from a vector - doAssert vec3(0, 0, 0).toAngles.eq vec3(0f, 0f, 0f) - doAssert vec3(0, 0, 1).toAngles.eq vec3(0f, 0f, 0f) # forward - doAssert vec3(0, 0, -1).toAngles.eq vec3(0f, PI, 0f) # back - doAssert vec3(-1, 0, 0).toAngles.eq vec3(0f, PI/2, 0f) # right - doAssert vec3(1, 0, 0).toAngles.eq vec3(0f, -PI/2, 0f) # left - doAssert vec3(0, 1, 0).toAngles.eq vec3(PI/2, 0f, 0f) # up - doAssert vec3(0, -1, 0).toAngles.eq vec3(-PI/2, 0f, 0f) # down - -block: - # test Euler angles from a matrix - doAssert translate(vec3(0, 0, 0)).toAngles.eq vec3(0f, 0f, 0f) - doAssert rotateX(0f).toAngles.eq vec3(0f, 0f, 0f) # forward - doAssert rotateY(PI).toAngles.eq vec3(0f, -PI, 0f) # back - doAssert rotateY(PI/2).toAngles.eq vec3(0f, PI/2, 0f) # back - doAssert rotateY(-PI/2).toAngles.eq vec3(0f, -PI/2, 0f) # back - doAssert rotateX(PI/2).toAngles.eq vec3(PI/2, 0f, 0f) # up - doAssert rotateX(-PI/2).toAngles.eq vec3(-PI/2, 0f, 0f) # down - doAssert rotateZ(PI/2).toAngles.eq vec3(0f, 0f, PI/2) # tilt right - doAssert rotateZ(-PI/2).toAngles.eq vec3(0f, 0f, -PI/2) # tilt left - - doAssert mat4().toAngles.eq vec3(0, 0, 0) - - doAssert rotateX(10.toRadians()).toAngles.eq vec3(10.toRadians(), 0, 0) - doAssert rotateY(10.toRadians()).toAngles.eq vec3(0, 10.toRadians(), 0) - doAssert rotateZ(10.toRadians()).toAngles.eq vec3(0, 0, 10.toRadians()) - doAssert rotateX(89.toRadians()).toAngles.eq vec3(89.toRadians(), 0, 0) - doAssert rotateY(89.toRadians()).toAngles.eq vec3(0, 89.toRadians(), 0) - doAssert rotateZ(89.toRadians()).toAngles.eq vec3(0, 0, 89.toRadians()) - doAssert rotateX(90.toRadians()).toAngles.eq vec3(90.toRadians(), 0, 0) - doAssert rotateY(90.toRadians()).toAngles.eq vec3(0, 90.toRadians(), 0) - doAssert rotateZ(90.toRadians()).toAngles.eq vec3(0, 0, 90.toRadians()) - doAssert rotateX(90.toRadians()).toAngles.eq vec3(90.toRadians(), 0, 0) - doAssert rotateY(90.toRadians()).toAngles.eq vec3(0, 90.toRadians(), 0) - doAssert rotateZ(-90.toRadians()).toAngles.eq vec3(0, 0, -90.toRadians()) - doAssert rotateY(180.toRadians()).toAngles.eq vec3(0, -180.toRadians(), 0) - doAssert rotateZ(180.toRadians()).toAngles.eq vec3(0, 0, 180.toRadians()) - doAssert rotateY(-180.toRadians()).toAngles.eq vec3(0, 180.toRadians(), 0) - doAssert rotateZ(-180.toRadians()).toAngles.eq vec3(0, 0, 180.toRadians()) - -block: - # Euler angles fuzzing tests. - - # Test fromAngles with and without roll have same forward - for i in 0 .. 1000: - let - xr = rand(-89.9f .. 89.9f).toRadians - yr = rand(-180 .. 180).toRadians - zr = rand(-180 .. 180).toRadians - a = vec3(xr, yr, zr) - b = vec3(xr, yr, 0f) - ma = fromAngles(a) - mb = fromAngles(b) - - doAssert ma.forward() ~= mb.forward() - - # Test forward/back, right/left, up/down combos - for i in 0 .. 1000: - let - xr = rand(-89.9f .. 89.9f).toRadians - yr = rand(-180 .. 180).toRadians - zr = rand(-180 .. 180).toRadians - b = vec3(xr, yr, zr) - m = fromAngles(b) +suite "double precision": + test "dmat constructors": + let m2 = dmat2(); let m3 = dmat3(); let m4 = dmat4() + check m2[0, 0] == 1.0 + check m3[1, 1] == 1.0 + check m4[2, 2] == 1.0 - doAssert m.forward() ~= m * vec3(0, 0, 1) - doAssert m.back() ~= m * vec3(0, 0, -1) + test "dmat element access": + var d4 = dmat4() + d4[0, 0] = 123.123 + check d4[0, 0] == 123.123 - doAssert m.right() ~= m * vec3(-1, 0, 0) - doAssert m.left() ~= m * vec3(1, 0, 0) + test "dmat transpose": + check dmat3().transpose() ~= dmat3() + check dmat4().transpose() ~= dmat4() - doAssert m.up() ~= m * vec3(0, 1, 0) - doAssert m.down() ~= m * vec3(0, -1, 0) + test "dmat scale translate rotate": + check scale(dvec2(1, 2)) ~= dmat3( + 1.0, 0.0, 0.0, + 0.0, 2.0, 0.0, + 0.0, 0.0, 1.0 + ) + check translate(dvec3(1, 2, 3)) ~= dmat4( + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + 1.0, 2.0, 3.0, 1.0 + ) - # Test non-polar and non-rotated cases - for i in 0 .. 1000: - let - xr = rand(-89.9f .. 89.9f).toRadians - yr = rand(-180 .. 180).toRadians - zr = 0f - b = vec3(xr, yr, zr) - m = fromAngles(b) - a = m.toAngles() - doAssert a.eq(b) - - # Test non-polar cases - for i in 0 .. 1000: - let - xr = rand(-89.9f .. 89.9f).toRadians - yr = rand(-180 .. 180).toRadians - zr = rand(-180 .. 180).toRadians - b = vec3(xr, yr, zr) - m = fromAngles(b) - a = m.toAngles() - doAssert a.eq(b) - - # Test polar and non-rotated cases - for i in 0 .. 1000: - let - xr = sample([-90, 90]).toRadians - yr = rand(-180 .. 180).toRadians - zr = 0f - b = vec3(xr, yr, zr) - m = fromAngles(b) - a = m.toAngles() - doAssert a.eq(b) - - # Test polar and crazy rotated cases - for i in 0 .. 1000: - let - xr = sample([-90, 90]).toRadians - yr = rand(-180 .. 180).toRadians - zr = rand(-180 .. 180).toRadians - b = vec3(xr, yr, zr) - m = fromAngles(b) - a = m.toAngles() - - doAssert abs(angleBetween(a.x, b.x)) < 0.001 - if xr > 0: - doAssert abs(angleBetween(a.y, b.y + b.z)) < 0.001 - else: - doAssert abs(angleBetween(a.y, b.y - b.z)) < 0.001 - -block: - # Test for https://github.com/treeform/vmath/issues/73 - template gen2DTestsFor(constructor: untyped): void = - doAssert angle(constructor(1, 0), constructor(1, 0)) ~= 0 - doAssert angle(constructor(1, 1), constructor(-1, -1)) ~= Pi - doAssert angle(constructor(1, 0), constructor(0, 1)) ~= Pi/2 - doAssert angle(constructor(1, 0), constructor(-1, 0)) ~= Pi - doAssert angle(constructor(1, 1), constructor(1, -1)) ~= Pi/2 - - # Edge cases: - doAssert angle(constructor(0, 0), constructor(1, 0)).isNaN() - - gen2DTestsFor vec2 - gen2DTestsFor dvec2 - - template gen3DTestsFor(constructor: untyped): void = - doAssert angle(constructor(1, 0, 0), constructor(1, 0, 0)) ~= 0 - doAssert angle(constructor(1, 1, 1), constructor(-1, -1, -1)) ~= Pi - doAssert angle(constructor(1, 0, 0), constructor(0, 1, 0)) ~= Pi/2 - doAssert angle(constructor(1, 0, 0), constructor(-1, 0, 0)) ~= Pi - doAssert angle(constructor(1, 1, 1), constructor(1, -1, 1)) ~= arccos(1/3) - doAssert angle(constructor(1, 0, 0), constructor(0, 0, 1)) ~= Pi/2 - doAssert angle(constructor(1, 1, 1), constructor(-1, -1, 1)) ~= arccos(-1/3) - - # Edge cases: - doAssert angle(vec3(0, 0, 0), vec3(1, 0, 0)).isNaN() - - gen3DTestsFor vec3 - gen3DTestsFor dvec3 - -echo "test finished successfully" + test "mat4 and dmat4 conversion": + let m = mat4( + 1, 2, 3, 4, + 5, 6, 7, 8, + 9, 10, 11, 12, + 13, 14, 15, 16 + ) + let d = dmat4(m) + let m2 = mat4(d) + check m ~= m2 + +suite "angle conversions": + test "toDegrees and toRadians": + check PI.toDegrees() == 180 + check (PI*2).toDegrees() == 360 + check 180.0.toRadians() ~= PI + check 360.0.toRadians() ~= PI*2 + +suite "position accessors": + test "mat3 pos": + check translate(vec2(1, 2)).pos == vec2(1, 2) + var m = translate(vec2(1, 2)) + m.pos = vec2(3, 4) + check m.pos == vec2(3, 4) + + test "mat4 pos": + check translate(vec3(1, 2, 3)).pos == vec3(1, 2, 3) + var m = translate(vec3(1, 2, 3)) + m.pos = vec3(3, 4, 5) + check m.pos == vec3(3, 4, 5) + +suite "angle between vectors": + test "vec2 angle": + check angle(vec2(1, 0), vec2(1, 0)) ~= 0 + check angle(vec2(1, 0), vec2(0, 1)) ~= PI/2 + check angle(vec2(1, 0), vec2(-1, 0)) ~= PI + check vmath.isNan(angle(vec2(0, 0), vec2(1, 0))) + + test "vec3 angle": + check angle(vec3(1, 0, 0), vec3(1, 0, 0)) ~= 0 + check angle(vec3(1, 0, 0), vec3(0, 1, 0)) ~= PI/2 + check angle(vec3(1, 0, 0), vec3(-1, 0, 0)) ~= PI + check vmath.isNan(angle(vec3(0, 0, 0), vec3(1, 0, 0))) diff --git a/tests/test_quaternion.nim b/tests/test_quaternion.nim deleted file mode 100644 index 5a454b8..0000000 --- a/tests/test_quaternion.nim +++ /dev/null @@ -1,91 +0,0 @@ -import - std/[math, random], - vmath - -randomize(4321) - -proc quatEquivalent[T](a, b: GVec4[T]): bool = - (a ~= b) or (a ~= (b * -1.0)) - -proc randomAxis(): DVec3 = - while true: - let axis = dvec3(rand(2.0) - 1.0, rand(2.0) - 1.0, rand(2.0) - 1.0) - if axis.length > 0.0001: - return axis.normalize() - -block: - # Axis-angle and quaternion constructors should agree on the same matrix. - doAssert quatRotateX(PI / 2).mat4() ~= rotateX(PI / 2) - doAssert quatRotateY(PI / 2).mat4() ~= rotateY(PI / 2) - doAssert quatRotateZ(PI / 2).mat4() ~= rotateZ(PI / 2) - - for _ in 0 ..< 2000: - let - axis = randomAxis() - angle = rand(-PI .. PI) - q = fromAxisAngle(axis, angle) - m = rotate(angle, axis) - doAssert q.mat4() ~= m - doAssert quatEquivalent(m.quat(), q) - doAssert m.quat().mat4() ~= m - -block: - # Known rotations should produce the expected quaternion values. - doAssert quatEquivalent(rotateX(PI / 2).quat(), quatRotateX(PI / 2)) - doAssert quatEquivalent(rotateY(PI / 2).quat(), quatRotateY(PI / 2)) - doAssert quatEquivalent(rotateZ(PI / 2).quat(), quatRotateZ(PI / 2)) - doAssert quatEquivalent(mat4().quat(), quat(0f, 0f, 0f, 1f)) - -block: - # Quaternion multiplication should compose rotations in matrix order. - let - qx = quatRotateX(0.37) - qy = quatRotateY(-0.91) - qz = quatRotateZ(1.24) - - mxy = rotateY(-0.91) * rotateX(0.37) - myz = rotateZ(1.24) * rotateY(-0.91) - mxyz = rotateZ(1.24) * rotateY(-0.91) * rotateX(0.37) - - doAssert quatMultiply(qx, qy).mat4() ~= mxy - doAssert quatMultiply(qy, qz).mat4() ~= myz - doAssert quatMultiply(quatMultiply(qx, qy), qz).mat4() ~= mxyz - -block: - # Rotating vectors with quaternion-derived matrices should match helpers. - let - x = dvec3(1, 0, 0) - y = dvec3(0, 1, 0) - z = dvec3(0, 0, 1) - - doAssert quatRotateY(PI / 2).mat4() * x ~= dvec3(0, 0, 1) - doAssert quatRotateX(PI / 2).mat4() * y ~= dvec3(0, 0, -1) - doAssert quatRotateZ(PI / 2).mat4() * x ~= dvec3(0, -1, 0) - doAssert quatRotate(quatRotateY(PI / 2), x) ~= dvec3(0, 0, 1) - doAssert quatRotate(quatRotateX(PI / 2), y) ~= dvec3(0, 0, -1) - doAssert quatRotate(quatRotateZ(PI / 2), x) ~= dvec3(0, -1, 0) - doAssert quatRotateY(PI / 2) * x ~= dvec3(0, 0, 1) - doAssert quatRotateX(PI / 2) * y ~= dvec3(0, 0, -1) - doAssert quatRotateZ(PI / 2) * x ~= dvec3(0, -1, 0) - doAssert fromTwoVectors(x, y).mat4() * x ~= y - doAssert fromTwoVectors(y, z).mat4() * y ~= z - - for _ in 0 ..< 2000: - let - a = randomAxis() - b = randomAxis() - q = fromTwoVectors(a, b) - v = randomAxis() - doAssert dist(q.mat4() * a, b) < 1e-5 - doAssert quatRotate(q, v) ~= q.mat4() * v - doAssert q * v ~= q.mat4() * v - -block: - # Basic arithmetic operators should still behave like vec4 math. - let - a = dquat(1, 2, 3, 4) - b = dquat(-0.5, 0.25, 2.0, -3.0) - doAssert a + b ~= dquat(0.5, 2.25, 5.0, 1.0) - doAssert a - b ~= dquat(1.5, 1.75, 1.0, 7.0) - doAssert a * 2.0 ~= dquat(2, 4, 6, 8) - doAssert b / 2.0 ~= dquat(-0.25, 0.125, 1.0, -1.5) diff --git a/vmath.nimble b/vmath.nimble index c9eb30c..396d743 100644 --- a/vmath.nimble +++ b/vmath.nimble @@ -1,4 +1,4 @@ -version = "2.0.1" +version = "3.0.0" author = "Andre von Houck" description = "Your single stop for vector math routines for 2d and 3d graphics." license = "MIT"