Skip to content

Inclusion of more linear algebra constructs #44

Closed

Description

@harrisi

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.

Activity

  1. crertel commented on Jul 9, 2024

    @crertel
    Owner

    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?

  2. harrisi commented on Jul 9, 2024

    @harrisi
    Author

    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:

    • Mat22 (@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()
    • Mat33
      • @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()
    • Mat44
      • @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 be number()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 about number(), maybe)
      • 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.

  3. andyleclair commented on Nov 14, 2024

    @andyleclair

    +1 from me! Happy to help here in any way I can.

  4. harrisi commented on Nov 14, 2024

    @harrisi
    Author

    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.

  5. crertel commented on Nov 26, 2024

    @crertel
    Owner

    Just a heads-up...I didn't miss this, but am thinking it over! Thank y'all for your input and patience!

  6. andyleclair commented on Feb 16, 2025

    @andyleclair

    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
  7. harrisi commented on Feb 16, 2025

    @harrisi
    Author

    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

    This looks more like a z-buffer issue. Enabling the depth test and clearing the depth buffer bit in your draw loop should help.

  8. andyleclair commented on Feb 17, 2025

    @andyleclair

    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
    

    This looks more like a z-buffer issue. Enabling the depth test and clearing the depth buffer bit in your draw loop should help.

    You would think that, but I think that I am doing it right? (code here, if you're interested andyleclair/glory#1)

  9. crertel commented on Feb 17, 2025

    @crertel
    Owner

    Please sir, my cubes, they are so horrific

    Odd, they appear :awesome: to me. :P

    Anyways, here's a freeze frame that shows the issue cleanly:

    Image

    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?).

  10. crertel commented on Feb 17, 2025

    @crertel
    Owner

    Looking at it a bit more, I think the problem is in your perspective function:

     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 = near - far
    
        {
          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, (-near - far) / range, 2.0 * far * near / range,
          0.0, 0.0, 1.0, 0.0
        }
      end

    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.

    You want something like:

    Image

    (from StackOverflow here)

    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. :)

  11. andyleclair commented on Feb 17, 2025

    @andyleclair

    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-L229

    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?

    Screen.Recording.2025-02-17.134653.mp4
  12. crertel commented on Feb 17, 2025

    @crertel
    Owner

    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?

  13. andyleclair commented on Feb 18, 2025

    @andyleclair

    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?

    Image

    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:

      @spec perspective(fov :: float(), aspect :: float(), near :: float(), far :: float()) ::
              Graphmath.Mat44.mat44()
      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
    
        # 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?

  14. crertel commented on Feb 18, 2025

    @crertel
    Owner

    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_transpose to help with this sort of thing.

  15. andyleclair commented on Feb 18, 2025

    @andyleclair

    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.

    Screen.Recording.2025-02-18.165829.mp4
  16. crertel commented on Feb 18, 2025

    @crertel
    Owner

    Oh neat! Any chance you can throw the two side-by-side and see where the terms disagree?

  17. andyleclair commented on Feb 19, 2025

    @andyleclair

    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.

  18. crertel commented on Sep 16, 2026

    @crertel
    Owner

    Thanks again to @harrisi and @andyleclair ; sorry for the (years-long) delay, but this stuff should be in a happier spot now. :)

  19. crertel commented on Sep 16, 2026

    @crertel
    Owner

    Completed the agreed feature work in #59–#67:

    • 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.

  20. andyleclair commented on Sep 16, 2026

    @andyleclair

    hell yeah, thank you!!

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions