Right-hand z-forward coordinate system

This commit is contained in:
treeform 2023-03-04 12:57:17 -08:00
parent 5c347516ba
commit 2688769103
3 changed files with 338 additions and 55 deletions

View file

@ -89,6 +89,44 @@ vmathObjArrayBased ................ 73.968 ms 74.292 ms ±0.631 x100
* [3d Ray Trace Benchmark](tests/bench_raytracer.nim)
* [2d SVG Render Benchmark](https://github.com/treeform/pixie/blob/master/tests/bench_svg.nim)
## Zmod - GLSL mod
GLSL uses a different type of float point mod. Because mod is a Nim keyword please use `zmod` when you need GLSL `mod` behavior.
## Coordinate System
Right-hand z-forward coordinate system
This is the same system used in the GLTF file format.
> glTF uses a right-handed coordinate system.
> glTF defines +Y as up, +Z as forward, and -X as right;
> the front of a glTF asset faces +Z.
[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 |
])
```
# 1.x.x to 2.0.0 vmath breaking changes:
* New right-hand-z-forward cordate system and functions that care about
coordinate system where moved there.
* deprecated `lookAt()` please use `toAngles()`/`fromAngles()` instead.
* deprecated `fractional()` use `frac()` instead.
# 0.x.x to 1.0.0 vmath breaking changes:
* `vec3(v)` no longer works please use `vec3(v.x, v.y, 0)` instead.

View file

@ -30,6 +30,8 @@ float32 float Vec2 Vec3 Vec4 Mat3 Mat4 Quat
float64 double DVec2 DVec3 DVec4 DMat3 DMat4 DQuat
======= ====== ===== ===== ===== ===== ===== =====
]##
import macros, math, strutils
@ -406,13 +408,17 @@ proc quantize*[T: SomeFloat](v, n: T): T =
## Makes v be multiple of n. Rounding to integer quantize by 1.0.
trunc(v / n) * n
proc fractional*[T: SomeFloat](v: T): T =
proc frac*[T: SomeFloat](v: T): T =
## Returns fractional part of a number.
## 3.14 -> 0.14
## -3.14 -> 0.14
result = abs(v)
result = result - trunc(result)
proc fractional*[T: SomeFloat](v: T): T {.deprecated: "Use frac() insetad"} =
## Returns fractional part of a number.
frac(v)
proc inversesqrt*[T: float32|float64](v: T): T =
## Returns inverse square root.
1/sqrt(v)
@ -425,11 +431,11 @@ proc mix*[T: SomeFloat](a, b, v: T): T =
v * (b - a) + a
proc fixAngle*[T: SomeFloat](angle: T): T =
## Make angle be from -PI to PI radians.
## Normalize the angle be from -PI to PI radians.
result = angle
while result > PI:
result -= PI * 2
while result < -PI:
while result <= -PI:
result += PI * 2
proc angleBetween*[T: SomeFloat](a, b: T): T =
@ -725,6 +731,11 @@ genMathFn(sqrt)
genMathFn(floor)
genMathFn(ceil)
genMathFn(abs)
genMathFn(trunc)
genMathFn(frac)
genMathFn(quantize)
genMathFn(toRadians)
genMathFn(toDegrees)
template genBoolFn(fn, op: untyped) =
proc fn*[T](a, b: GVec2[T]): BVec2 =
@ -970,10 +981,42 @@ proc `pos=`*[T](a: var GMat3[T], pos: GVec2[T]) =
a[2, 0] = pos.x
a[2, 1] = pos.y
proc forward*[T](a: GMat4[T]): GVec3[T] {.inline.} =
## Vector facing +Z.
result.x = a[2, 0]
result.y = a[2, 1]
result.z = a[2, 2]
proc back*[T](a: GMat4[T]): GVec3[T] {.inline.} =
## Vector facing -Z.
-a.forward()
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]
proc right*[T](a: GMat4[T]): GVec3[T] {.inline.} =
## Vector facing -X.
-a.left()
proc up*[T](a: GMat4[T]): GVec3[T] {.inline.} =
## Vector facing +Y.
result.x = a[1, 0]
result.y = a[1, 1]
result.z = a[1, 2]
proc down*[T](a: GMat4[T]): GVec3[T] {.inline.} =
## Vector facing -X.
-a.up()
proc pos*[T](a: GMat4[T]): GVec3[T] =
## Position of the matrix.
gvec3[T](a[3].x, a[3].y, a[3].z)
proc `pos=`*[T](a: var GMat4[T], pos: GVec3[T]) =
## See the position of the matrix.
a[3, 0] = pos.x
a[3, 1] = pos.y
a[3, 2] = pos.z
@ -1244,7 +1287,7 @@ proc translate*[T](v: GVec3[T]): GMat4[T] =
)
proc rotate*[T](angle: T): GMat3[T] =
## Create a rotation matrix by an angle.
## Create a 2d rotation matrix by an angle.
let
sin = sin(angle)
cos = cos(angle)
@ -1254,22 +1297,119 @@ proc rotate*[T](angle: T): GMat3[T] =
0, 0, 1
)
proc hrp*[T](m: GMat4[T]): GVec3[T] =
## Return heading, rotation and pivot of a matrix.
var heading, pitch, roll: float32
if m[1] > 0.998: # singularity at north pole
heading = arctan2(m[2], m[10])
pitch = PI / 2
roll = 0
elif m[1] < -0.998: # singularity at south pole
heading = arctan2(m[2], m[10])
pitch = -PI / 2
roll = 0
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)
proc rotateX*[T](angle: T): GMat4[T] =
## Return a rotation matrix around X with angle.
result[0, 0] = 1
result[0, 1] = 0
result[0, 2] = 0
result[0, 3] = 0
result[1, 0] = 0
result[1, 1] = cos(angle)
result[1, 2] = -sin(angle)
result[1, 3] = 0
result[2, 0] = 0
result[2, 1] = sin(angle)
result[2, 2] = cos(angle)
result[2, 3] = 0
result[3, 0] = 0
result[3, 1] = 0
result[3, 2] = 0
result[3, 3] = 1
proc rotateY*[T](angle: T): GMat4[T] =
## Return a rotation matrix around Y with angle.
result[0, 0] = cos(angle)
result[0, 1] = 0
result[0, 2] = sin(angle)
result[0, 3] = 0
result[1, 0] = 0
result[1, 1] = 1
result[1, 2] = 0
result[1, 3] = 0
result[2, 0] = -sin(angle)
result[2, 1] = 0
result[2, 2] = cos(angle)
result[2, 3] = 0
result[3, 0] = 0
result[3, 1] = 0
result[3, 2] = 0
result[3, 3] = 1
proc rotateZ*[T](angle: T): GMat4[T] =
## Return a rotation matrix around Z with angle.
result[0, 0] = cos(angle)
result[0, 1] = -sin(angle)
result[0, 2] = 0
result[0, 3] = 0
result[1, 0] = sin(angle)
result[1, 1] = cos(angle)
result[1, 2] = 0
result[1, 3] = 0
result[2, 0] = 0
result[2, 1] = 0
result[2, 2] = 1
result[2, 3] = 0
result[3, 0] = 0
result[3, 1] = 0
result[3, 2] = 0
result[3, 3] = 1
proc toAngles*[T](a: GVec3[T]): GVec3[T] =
## Given a 3d vector, computes euler angles: pitch and yaw
## pitch (x rotation)
## yaw (y rotation)
## roll (z rotation) - always 0 in vector case
if a == vec3(0, 0, 0):
return
let
yaw = -arctan2(a.x, a.z)
pitch = -arctan2(sqrt(a.x*a.x + a.z*a.z), a.y) + T(PI/2)
result.x = pitch.fixAngle
result.y = yaw.fixAngle
proc toAngles*[T](origin, target: GVec3[T]): GVec3[T] =
## Gives euler angles from origin to target
## pitch (x rotation)
## yaw (y rotation)
## roll (z rotation) - always 0 in vector case
toAngles(target - origin)
proc toAngles*[T](m: GMat4[T]): GVec3[T] =
## Decomposes the matrix into euler angles:
## pitch (x rotation)
## yaw (y rotation)
## roll (z rotation)
## Assumes matrix has not been scaled.
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])
else:
heading = arctan2(-m[8], m[0])
pitch = arctan2(-m[6], m[5])
roll = arcsin(m[4])
gvec3[T](heading, pitch, roll)
# Normal case.
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.
rotateY(a.y) * rotateX(a.x) * rotateZ(a.z)
proc frustum*[T](left, right, bottom, top, near, far: T): GMat4[T] =
## Create a frustum matrix.
@ -1332,7 +1472,10 @@ 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] =
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".} =
## Create a matrix that would convert eye pos to looking at center.
let
eyex = eye[0]
@ -1413,7 +1556,10 @@ 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] =
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))
@ -1598,17 +1744,6 @@ proc rotate*[T](angle: T, axis: GVec3[T]): GMat4[T] =
## Return a rotation matrix with axis and angle.
fromAxisAngle(axis, angle).mat4()
proc rotateX*[T](angle: T): GMat4[T] =
## Return a rotation matrix around X with angle.
fromAxisAngle(gvec3[T](1, 0, 0), angle).mat4()
proc rotateY*[T](angle: T): GMat4[T] =
## Return a rotation matrix around Y with angle.
fromAxisAngle(gvec3[T](0, 1, 0), angle).mat4()
proc rotateZ*[T](angle: T): GMat4[T] =
## Return a rotation matrix around Z with angle.
fromAxisAngle(gvec3[T](0, 0, 1), angle).mat4()
when defined(release):
{.pop.}

View file

@ -863,29 +863,7 @@ block:
)
block:
# test quat and matrix lookat
doAssert lookAt(vec3(1, 2, 3), vec3(0, 0, 0)).quat ~=
quat(
0.07232953608036041,
0.3063928484916687,
0.9237624406814575,
0.2180707305669785
)
doAssert lookAt(vec3(0, 0, 0), vec3(0, 0, 0)).quat ~= quat(0.0, 0.0, 0.0, 1.0)
doAssert lookAt(vec3(1, 0, 0), vec3(0, 0, 0)).quat ~= quat(0.5, 0.5, 0.5, 0.5)
doAssert lookAt(vec3(0, 1, 0), vec3(0, 0, 0)).quat ~=
quat(
0.0,
0.7071067690849304,
0.7071067690849304,
0.0
)
doAssert lookAt(vec3(0, 0, 1), vec3(0, 0, 0)).quat ~= quat(0.0, 0.0, 0.0, 1.0)
let
a = lookAt(vec3(1, 2, 3), vec3(0, 0, 0))
b = lookAt(dvec3(1, 2, 3), dvec3(0, 0, 0))
# 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,
@ -1045,4 +1023,136 @@ block:
b = a / 2
when compiles(b = a div 2): doAssert false # type mismatch
proc closeAngles(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.closeAngles vec3(0f, 0f, 0f)
doAssert vec3(0, 0, 1).toAngles.closeAngles vec3(0f, 0f, 0f) # forward
doAssert vec3(0, 0, -1).toAngles.closeAngles vec3(0f, PI, 0f) # back
doAssert vec3(-1, 0, 0).toAngles.closeAngles vec3(0f, PI/2, 0f) # right
doAssert vec3(1, 0, 0).toAngles.closeAngles vec3(0f, -PI/2, 0f) # left
doAssert vec3(0, 1, 0).toAngles.closeAngles vec3(PI/2, 0f, 0f) # up
doAssert vec3(0, -1, 0).toAngles.closeAngles vec3(-PI/2, 0f, 0f) # down
block:
# test Euler angles from a matrix
doAssert translate(vec3(0, 0, 0)).toAngles.closeAngles vec3(0f, 0f, 0f)
doAssert rotateX(0f).toAngles.closeAngles vec3(0f, 0f, 0f) # forward
doAssert rotateY(PI).toAngles.closeAngles vec3(0f, -PI, 0f) # back
doAssert rotateY(PI/2).toAngles.closeAngles vec3(0f, PI/2, 0f) # back
doAssert rotateY(-PI/2).toAngles.closeAngles vec3(0f, -PI/2, 0f) # back
doAssert rotateX(PI/2).toAngles.closeAngles vec3(PI/2, 0f, 0f) # up
doAssert rotateX(-PI/2).toAngles.closeAngles vec3(-PI/2, 0f, 0f) # down
doAssert rotateZ(PI/2).toAngles.closeAngles vec3(0f, 0f, PI/2) # tilt right
doAssert rotateZ(-PI/2).toAngles.closeAngles vec3(0f, 0f, -PI/2) # tilt left
doAssert Mat4().toAngles.closeAngles vec3(0, 0, 0)
doAssert rotateX(10.toRadians()).toAngles.closeAngles vec3(10.toRadians(), 0, 0)
doAssert rotateY(10.toRadians()).toAngles.closeAngles vec3(0, 10.toRadians(), 0)
doAssert rotateZ(10.toRadians()).toAngles.closeAngles vec3(0, 0, 10.toRadians())
doAssert rotateX(89.toRadians()).toAngles.closeAngles vec3(89.toRadians(), 0, 0)
doAssert rotateY(89.toRadians()).toAngles.closeAngles vec3(0, 89.toRadians(), 0)
doAssert rotateZ(89.toRadians()).toAngles.closeAngles vec3(0, 0, 89.toRadians())
doAssert rotateX(90.toRadians()).toAngles.closeAngles vec3(90.toRadians(), 0, 0)
doAssert rotateY(90.toRadians()).toAngles.closeAngles vec3(0, 90.toRadians(), 0)
doAssert rotateZ(90.toRadians()).toAngles.closeAngles vec3(0, 0, 90.toRadians())
doAssert rotateX(90.toRadians()).toAngles.closeAngles vec3(90.toRadians(), 0, 0)
doAssert rotateY(90.toRadians()).toAngles.closeAngles vec3(0, 90.toRadians(), 0)
doAssert rotateZ(-90.toRadians()).toAngles.closeAngles vec3(0, 0, -90.toRadians())
doAssert rotateY(180.toRadians()).toAngles.closeAngles vec3(0, -180.toRadians(), 0)
doAssert rotateZ(180.toRadians()).toAngles.closeAngles vec3(0, 0, 180.toRadians())
doAssert rotateY(-180.toRadians()).toAngles.closeAngles vec3(0, 180.toRadians(), 0)
doAssert rotateZ(-180.toRadians()).toAngles.closeAngles 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)
doAssert m.forward() ~= m * vec3(0, 0, 1)
doAssert m.back() ~= m * vec3(0, 0, -1)
doAssert m.right() ~= m * vec3(-1, 0, 0)
doAssert m.left() ~= m * vec3(1, 0, 0)
doAssert m.up() ~= m * vec3(0, 1, 0)
doAssert m.down() ~= m * vec3(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.closeAngles(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.closeAngles(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.closeAngles(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
echo "test finished successfully"