You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hi, thanks for graphmath! It solves a lot of problems with 3D rendering and physics simulations.
There are a few things that could be added to make this more complete for realtime 3D interactive applications, as well as 2D UIs, such as Scenic.
A few things such as Vec4, Mat22, some added functions such as trace, determinant, look_at, perspective, ortho, maybe some others, would be nice for both 3D rendering and some physics-based simulations.
The main interest I have is to unify many of these common math functions into a package that something like Scenic could utilize, as well as my own projects with OpenGL rendering. I'm not sure if this is outside the scope of this project, but if it seems it would fit, I'd be happy to open a (or some) PRs.
@spec look_at(eye :: Vec3.t(), center :: Vec3.t(), up :: Vec3.t()) :: t()
@spec perspective(fov_y :: float(), aspect :: float(), near :: float(), far :: float()) :: t() (although these should maybe be number()s, now that I'm looking at it)
I have something like @spec multiply_vec(Mat44.t(), Vec4.t()) :: Vec4.t() which returns a new Vec4 with the dot product of each row by the vector. I think the same thing can be achieved already with other functions though.
Vec4
normal vector stuff, add, subtract, dot, normalize, scale, etc.
I don't have a strong opinion on Mat34/Mat23. I haven't had a need for something like that yet. I don't love the non-square nature, since really you'd need Mat43 and Mat32 as well. But I suppose that's not the end of the world.
Let me know if some or all of those seem unnecessary.
I'd like to say that I think some of my suggestions are probably unnecessary, or even incorrect, abstractions to expose. I'm not sure. Personally, Vec4 and some of the camera-related functions could be easy additions.
Alternatively, targeting some level of feature parity with something like glm would be an option. I'm not sure if that would be more fit as a separate package, potentially just being a NIF wrapper to glm.
Anyways, here's a freeze frame that shows the issue cleanly:
I was at first gonna suggest checking your normals, but I don't know if you have backface culling even on. Also, the observed behavior doesn't match that.
My next guess was the depth check function--perhaps a change the test to more instead of less and see if that helps (there might be a sign error in the projection?).
So, the top two rows are cromulent...but the last row should be a -1 and the third row would be wrong if it weren't for the range = near - far resulting in a surprise negative.
Songha is also a great resource for all the different ways of mathemattically skinning this problem, check out here.
Maybe try:
def perspective(fov, aspect, near, far)
when is_float(fov) and is_float(aspect) and is_float(near) and is_float(far) do
fov_rad = radians(fov / 2.0)
tan_half_fovy = :math.tan(fov_rad)
range = far - near
{
1.0 / (aspect * tan_half_fovy), 0.0, 0.0, 0.0,
0.0, 1.0 / tan_half_fovy, 0.0, 0.0,
0.0, 0.0, -1.0 * (far + near) / range, - 2.0 * far * near / range,
0.0, 0.0, -1.0, 0.0
}
end
...FWIW I'd also suggest just writing out the range term to make it look more like the textbooks and resources for people like me whose memory is fuzzy. :)
Every implementation varies slightly! The version you've provided me (thanks!) also does this, which is different, but still not right!
Screen.Recording.2025-02-17.133021.mp4
I wonder if something is truly wrongo here, because I've also coded up a simple orthographic projection based on Songha's posts, and that doesn't work either. Are my vertices wildly out of order or something?
The fact that the texturing isn't totally fried seems to me to be a sign that the vertices are in the right order--consistent winding is maybe worth looking at, though (since that would impact normals and hence backface culling). What's your OS/hardware?
So, I'm on WSL. I'm running this code in Linux. Microsoft says this should work, and when I run the C++ example from learnopengl (this was a truly cursed experience and I count my lucky stars daily that I am paid to write Elixir) it works! So, I don't think there's something inherently wrong with my setup. I can make a cube and look at him, he is beautiful.
Screen.Recording.2025-02-17.211014.mp4
This brings me back to my code. Am I just screwing up the orientation of the matrices?
And when I do that I get a much more reasonable (albeit still wrong!) result, but in order to get this, I need to transpose my model rotation and view translation matrices. If I don't transpose them (which I am using graphmath to calculate) nothing shows up at all.
Screen.Recording.2025-02-17.214550.mp4
Now my perspective looks like this:
@specperspective(fov::float(),aspect::float(),near::float(),far::float())::Graphmath.Mat44.mat44()defperspective(fov,aspect,near,far)whenis_float(fov)andis_float(aspect)andis_float(near)andis_float(far)dofov_rad=radians(fov/2.0)tan_half_fovy=:math.tan(fov_rad)range=far-near# Written out like this so it's easier to think about# Remember, OpenGL matrices are column-major# { 1.0 / (aspect * tan_half_fovy), 0.0, 0.0, 0.0,# 0.0, 1.0 / tan_half_fovy, 0.0, 0.0,# 0.0, 0.0, -((far + near) / range), -1.0,# 0.0, 0.0, -(2.0 * far * near / range), 0.0# }{1.0/(aspect*tan_half_fovy),0.0,0.0,0.0,0.0,1.0/tan_half_fovy,0.0,0.0,0.0,0.0,-((far+near)/range),-1.0,0.0,0.0,-(2.0*far*near/range),0.0}end
I think I was getting it twisted originally, because I was reading Songho's examples, but I was typing them into my tuples row-major!
Given a matrix like this:
a b c d
1
2
3
4
The tuple layout that opengl is expecting is like this:
{a1,a2,a3,a4,b1...}
But I was writing it like
{a1,b2,c1,d1,a2...}
And obviously now that I see it, clearly that's wrong!
Does the column-major-ness of OpenGL have implications with Graphmath?
Success!! I pulled in e3d from wings (https://github.com/dgud/wings/tree/master/e3d) which has a bunch of functions, amongst which are matrix rotations and perspective! That allowed me to figure out that I do think it was the Graphmath functions that were causing me issues.
Sure! So, I think that I was right about the order for perspective, because once I was using the e3d rotation and translation to move the cube into a reasonable place, I switched back to my implementation and it worked!
At this point, now I'm not sure what I was getting so wrong? I have the code working with both Graphmath and e3d (although I'm getting a weird perspective distortion when I rotate with e3d). Initially I think what I was getting wrong was just passing nonsensical values for the rotation? Honestly I'm not sure. I feel like I'm taking crazy pills! I think I got the units wrong here (needed to be radians(90)?) andyleclair/glory@e912b57#diff-2e6dd8dc11c7a624423ff10e691312f941a51c6f01e6ab030675213f17842dc6R229 and maybe that was throwing it off big time?
Either way, I've learned a lot fumbling about in the dark here, so, thank you! I'm happy to add a PR with my perspective and ortho functions, but I have no godly idea how I would add unit tests.
Mat22 and Vec4, including explicit homogeneous point/direction constructors.
Mat33/Mat44 trace, determinant, submatrix, and cofactor operations, plus Mat33 orthonormalization. The requested matrix-returning minor operation is named submatrix; matrix-vector multiplication is already available through apply/2.
Reflection and shear constructors, with explicit 2D/3D names where Mat33 is ambiguous.
Mat44 orientation, look-at, spherical and axis-constrained billboards, perspective, and orthographic projections.
The merged suite passes 597 tests and 7 doctests with 100% line coverage. Rectangular matrices are deferred; broader GLM parity or a native wrapper can be considered separately.
Activity
crertel commentedon Jul 9, 2024
That's exactly up the alley of this project--and as a maintainer on Scenic, I'm very aware of how it might help there. :)
If you can put together a list of things (similar to what you already have, maybe add some additional stuff), then I can get that plumbed in.
I also feel like a 3x4 matrix or 2x3 matrix for 2D stuff without shear could be useful. What do you think?
harrisi commentedon Jul 9, 2024
Awesome! I realized after the fact when I was going to ask Scenic people what they'd need from graphmath. Turns out I don't need to! :)
Here's a list of things that I've had to implement to supplement graphmath so far, for various 2D and 3D projects:
@type t :: {Vec2.t(), Vec2.t()})@spec scale(mat :: t(), scalar :: number()) :: t()@spec add(mat1 :: t(), mat2 :: t()) :: t()@spec determinant(mat :: t()) :: float()@spec trace(mat :: t()) :: float()@spec determinant(mat :: t()) :: float()@spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat22.t()@spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float()@spec trace(mat :: t()) :: float()@spec determinant(mat :: t()) :: float()@spec minor(mat :: t(), i :: integer(), j :: integer()) :: Mat33.t()@spec cofactor(mat :: t(), i :: integer(), j :: integer()) :: float()@spec orient(pos :: Vec3.t(), fwd :: Vec3.t(), up :: Vec3.t()) :: t()@spec look_at(eye :: Vec3.t(), center :: Vec3.t(), up :: Vec3.t()) :: t()@spec perspective(fov_y :: float(), aspect :: float(), near :: float(), far :: float()) :: t()(although these should maybe benumber()s, now that I'm looking at it)@spec ortho(x_min :: float(), x_max :: float(), y_min :: float(), y_max :: float(), z_near :: float(), z_far :: float()) :: t()(ditto aboutnumber(), maybe)@spec multiply_vec(Mat44.t(), Vec4.t()) :: Vec4.t()which returns a newVec4with the dot product of each row by the vector. I think the same thing can be achieved already with other functions though.I don't have a strong opinion on
Mat34/Mat23. I haven't had a need for something like that yet. I don't love the non-square nature, since really you'd needMat43andMat32as well. But I suppose that's not the end of the world.Let me know if some or all of those seem unnecessary.
andyleclair commentedon Nov 14, 2024
+1 from me! Happy to help here in any way I can.
harrisi commentedon Nov 14, 2024
I'd like to say that I think some of my suggestions are probably unnecessary, or even incorrect, abstractions to expose. I'm not sure. Personally,
Vec4and some of the camera-related functions could be easy additions.Alternatively, targeting some level of feature parity with something like
glmwould be an option. I'm not sure if that would be more fit as a separate package, potentially just being a NIF wrapper toglm.crertel commentedon Nov 26, 2024
Just a heads-up...I didn't miss this, but am thinking it over! Thank y'all for your input and patience!
andyleclair commentedon Feb 16, 2025
Hi! I'm here, I'm still looking for a working projection matrix. Please sir, my cubes, they are so horrific
Screen.Recording.2025-02-16.000702.mp4
harrisi commentedon Feb 16, 2025
This looks more like a z-buffer issue. Enabling the depth test and clearing the depth buffer bit in your draw loop should help.
andyleclair commentedon Feb 17, 2025
You would think that, but I think that I am doing it right? (code here, if you're interested andyleclair/glory#1)
crertel commentedon Feb 17, 2025
Odd, they appear
:awesome:to me. :PAnyways, here's a freeze frame that shows the issue cleanly:
I was at first gonna suggest checking your normals, but I don't know if you have backface culling even on. Also, the observed behavior doesn't match that.
My next guess was the depth check function--perhaps a change the test to more instead of less and see if that helps (there might be a sign error in the projection?).
crertel commentedon Feb 17, 2025
Looking at it a bit more, I think the problem is in your perspective function:
So, the top two rows are cromulent...but the last row should be a
-1and the third row would be wrong if it weren't for therange = near - farresulting in a surprise negative.You want something like:
(from StackOverflow here)
Songha is also a great resource for all the different ways of mathemattically skinning this problem, check out here.
Maybe try:
...FWIW I'd also suggest just writing out the
rangeterm to make it look more like the textbooks and resources for people like me whose memory is fuzzy. :)andyleclair commentedon Feb 17, 2025
I've been through Songha's posts several times (also linked from the OpenGL tutorial I'm following along with: https://learnopengl.com/Getting-started/Coordinate-Systems) but I was beginning to think i just like, had the orientation wrong? On learnopengl, they're using GLM, which supplies
perspective, so I went to go read the source https://github.com/g-truc/glm/blob/0.9.5/glm/gtc/matrix_transform.inl#L207-L229Every implementation varies slightly! The version you've provided me (thanks!) also does this, which is different, but still not right!
Screen.Recording.2025-02-17.133021.mp4
I wonder if something is truly wrongo here, because I've also coded up a simple orthographic projection based on Songha's posts, and that doesn't work either. Are my vertices wildly out of order or something?
Screen.Recording.2025-02-17.134653.mp4
crertel commentedon Feb 17, 2025
The fact that the texturing isn't totally fried seems to me to be a sign that the vertices are in the right order--consistent winding is maybe worth looking at, though (since that would impact normals and hence backface culling). What's your OS/hardware?
andyleclair commentedon Feb 18, 2025
So, I'm on WSL. I'm running this code in Linux. Microsoft says this should work, and when I run the C++ example from learnopengl (this was a truly cursed experience and I count my lucky stars daily that I am paid to write Elixir) it works! So, I don't think there's something inherently wrong with my setup. I can make a cube and look at him, he is beautiful.
Screen.Recording.2025-02-17.211014.mp4
This brings me back to my code. Am I just screwing up the orientation of the matrices?
When I look at Songho's example compared to this code sample from @harrisi https://github.com/harrisi/elixir_threed/blob/main/lib/three_d/mat4.ex#L51 It would seem like I'm getting the matrix transposed, yeah? The -1 should be in a different place?
And when I do that I get a much more reasonable (albeit still wrong!) result, but in order to get this, I need to transpose my model rotation and view translation matrices. If I don't transpose them (which I am using graphmath to calculate) nothing shows up at all.
Screen.Recording.2025-02-17.214550.mp4
Now my
perspectivelooks like this:I think I was getting it twisted originally, because I was reading Songho's examples, but I was typing them into my tuples row-major!
Given a matrix like this:
The tuple layout that opengl is expecting is like this:
But I was writing it like
And obviously now that I see it, clearly that's wrong!
Does the column-major-ness of OpenGL have implications with Graphmath?
crertel commentedon Feb 18, 2025
Ah, I believe (and I'm rather embarrassed that I don't know this off the top of my head cold) that I wrote Graphmath to be row major (see here).
If it's any help, we have
multiply_transposeto help with this sort of thing.andyleclair commentedon Feb 18, 2025
Success!! I pulled in
e3dfromwings(https://github.com/dgud/wings/tree/master/e3d) which has a bunch of functions, amongst which are matrix rotations and perspective! That allowed me to figure out that I do think it was the Graphmath functions that were causing me issues.Screen.Recording.2025-02-18.165829.mp4
crertel commentedon Feb 18, 2025
Oh neat! Any chance you can throw the two side-by-side and see where the terms disagree?
andyleclair commentedon Feb 19, 2025
Sure! So, I think that I was right about the order for
perspective, because once I was using thee3drotation and translation to move the cube into a reasonable place, I switched back to my implementation and it worked!At this point, now I'm not sure what I was getting so wrong? I have the code working with both Graphmath and e3d (although I'm getting a weird perspective distortion when I rotate with e3d). Initially I think what I was getting wrong was just passing nonsensical values for the rotation? Honestly I'm not sure. I feel like I'm taking crazy pills! I think I got the units wrong here (needed to be
radians(90)?) andyleclair/glory@e912b57#diff-2e6dd8dc11c7a624423ff10e691312f941a51c6f01e6ab030675213f17842dc6R229 and maybe that was throwing it off big time?Either way, I've learned a lot fumbling about in the dark here, so, thank you! I'm happy to add a PR with my perspective and ortho functions, but I have no godly idea how I would add unit tests.
crertel commentedon Sep 16, 2026
Thanks again to @harrisi and @andyleclair ; sorry for the (years-long) delay, but this stuff should be in a happier spot now. :)
crertel commentedon Sep 16, 2026
Completed the agreed feature work in #59–#67:
minoroperation is namedsubmatrix; matrix-vector multiplication is already available throughapply/2.The merged suite passes 597 tests and 7 doctests with 100% line coverage. Rectangular matrices are deferred; broader GLM parity or a native wrapper can be considered separately.
andyleclair commentedon Sep 16, 2026
hell yeah, thank you!!