graphmath icon indicating copy to clipboard operation
graphmath copied to clipboard

Inclusion of more linear algebra constructs

Open harrisi opened this issue 1 year ago • 17 comments

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.

harrisi avatar Jul 08 '24 21:07 harrisi

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?

crertel avatar Jul 09 '24 05:07 crertel

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.

harrisi avatar Jul 09 '24 16:07 harrisi

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

andyleclair avatar Nov 14 '24 21:11 andyleclair

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.

harrisi avatar Nov 14 '24 22:11 harrisi

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

crertel avatar Nov 26 '24 01:11 crertel

Hi! I'm here, I'm still looking for a working projection matrix. Please sir, my cubes, they are so horrific

https://github.com/user-attachments/assets/e095951d-5b43-4dd2-ba77-d57a99d1d07e

andyleclair avatar Feb 16 '25 19:02 andyleclair

Hi! I'm here, I'm still looking for a working projection matrix. Please sir, my cubes, they are so horrific

https://github.com/user-attachments/assets/e095951d-5b43-4dd2-ba77-d57a99d1d07e

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

harrisi avatar Feb 16 '25 22:02 harrisi

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 https://github.com/andyleclair/gltest/pull/1)

andyleclair avatar Feb 17 '25 03:02 andyleclair

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

crertel avatar Feb 17 '25 10:02 crertel

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

crertel avatar Feb 17 '25 11:02 crertel

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!

https://github.com/user-attachments/assets/afe87355-203f-46ae-9dbf-057adeb59848

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?

https://github.com/user-attachments/assets/e22d9612-6c6e-43b4-806c-79ac93b2aaa3

andyleclair avatar Feb 17 '25 18:02 andyleclair

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?

crertel avatar Feb 17 '25 22:02 crertel

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.

https://github.com/user-attachments/assets/30941686-284a-4371-8b22-ff56c9fcf360

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.

https://github.com/user-attachments/assets/079a535d-7991-4c00-8c96-c890c0ee1b91

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?

andyleclair avatar Feb 18 '25 03:02 andyleclair

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.

crertel avatar Feb 18 '25 04:02 crertel

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.

https://github.com/user-attachments/assets/3c3a7482-aa6f-4d08-859f-1909127f9794

andyleclair avatar Feb 18 '25 22:02 andyleclair

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

crertel avatar Feb 18 '25 22:02 crertel

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)?) https://github.com/andyleclair/gltest/pull/1/commits/e912b57c61ea737c98eb6871e6e1ec14a28e2efe#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.

andyleclair avatar Feb 19 '25 03:02 andyleclair