Skip to content

Conversation

@gselzer
Copy link
Collaborator

@gselzer gselzer commented Jul 11, 2025

This PR adds the Camera.projection field, which enables the arbitrary customization of the camera frustum. This is a huge step towards solving #16, as it provides a uniform way to unproject a canvas position into a ray passing through the world.

The changeset needs a fair bit of work before merge:

Immediate discussion points:

  • We will need utilities for creating common projection matrices. For now I've stored them in a new file src/scenex/utils/projections.py, however I'm still looking for something better.
    • I am concerned that this change represents in additional complexity for users, but I'm hopeful that these utilities can minimize the complexity.
  • Which Camera fields could be computed from the projection matrix and/or removed?
    • type should likely be removed, as it ties heavily into the idea of the projection matrix
    • zoom might become a computed field?

TODOs:

  • Enable in vispy
  • Test computations
  • Documentation. In particular, the projection creator utilities should be very well documented.
  • Clean up utilities
    • In particular, the perspective utility parameters are...too much 😅
  • Consider removing CameraAdaptor._snx_zoom_to_fit, in favor of mathematically computing the bounds and setting the camera transform and projection

gselzer added 2 commits July 10, 2025 16:57
Defines how 2D view NDC are mapped to vectors in 3D space
@gselzer gselzer self-assigned this Jul 11, 2025
@gselzer gselzer added the enhancement New feature or request label Jul 11, 2025
@codecov
Copy link

codecov bot commented Jul 11, 2025

Codecov Report

Attention: Patch coverage is 98.30508% with 1 line in your changes missing coverage. Please review.

Project coverage is 81.96%. Comparing base (5e325f0) to head (9fbeae3).

Files with missing lines Patch % Lines
src/scenex/utils/projections.py 94.44% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #27      +/-   ##
==========================================
+ Coverage   80.83%   81.96%   +1.12%     
==========================================
  Files          42       43       +1     
  Lines        1529     1558      +29     
==========================================
+ Hits         1236     1277      +41     
+ Misses        293      281      -12     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 43 to 46
projection: Transform = Field(
default_factory=Transform,
description="Describes how 3D points are mapped to a 2D canvas",
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is indeed a very important thing for us to represent somehow here. And, the most important (and difficult) thing here is to represent this in a way such that the model has a single source of truth. zoom and center, for example, (and fov which would eventually have had if we were mimicking the vispy model) are also just ways to represent the projection transform.

I do very much like the concept of having a single projection transform as the fundamental source of truth, however. And I would be fully in support of removing any conflicting fields (like zoom) from the model, and instead replace them with convenience methods like set_zoom() or look_at() which in turn update the projection transform. (and, because those decomposed things like fov, aspect, etc... are all things that users very often want to know, it is convenient to have ways to go back and forth between composed and decomposed components... similar to what pygfx does in their Affine model)

so: high level. I'm a fan of this change, but want to see it replace zoom and everything else that it can

Copy link
Member

@tlambert03 tlambert03 Jul 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the way ... we're touching on similar issues here to tlambert03/microvis#47 ... and related discussions in tlambert03/microvis#38

not that any of that necessarily had it "figure out" in a nice clean model. but it's possible the discussions/code have some interest here (buy perhaps not :))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so: high level. I'm a fan of this change, but want to see it replace zoom and everything else that it can

Fully agree with you. I'm thinking that for now I'd like to remove zoom from the camera model entirely (and probably center as well, although it's less related to this change), and we can add computed fields back into the model later if they make sense.

@gselzer gselzer force-pushed the camera-projection branch from 7b30c5f to 09e5b4d Compare July 15, 2025 01:51
gselzer added 8 commits July 15, 2025 09:27
Thanks to @tlambert03 for the suggestion. Could probably be cleaned up
further, but at least this is a step in the right direction!
Eventually, we'll want to compute grids ourselves on the model side, I
think, but for now this works
...man, this feels so good
Copy link
Collaborator Author

@gselzer gselzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tlambert03 I'm getting pretty happy with this, do you want to take more of a look?

There are definitely semi-related features that I'd like to add outside of this PR, including:

  • more utilities for matrix construction
  • Model-based computation of zoom-to-fit
  • Model-based view sizing

There are also a few scattered FIXMEs still that I want to get to in the coming days, but I don't think their presence will affect your review:

  • Remove the Camera.type field from the Camera model - just need to address its disappearance in the pygfx adaptor.
  • Fix the Camera.projection default.
  • Add a tests/adaptors/_pygfx/test_camera file analogous to the vispy one. Those tests should be super simple.

Comment on lines +31 to +46
# Translate the camera to the center of the volume, and distance the camera from the
# volume in the z dimension (important for perspective transforms)
view.camera.transform = Transform().translated((127.5, 127.5, 300))

# view.camera.projection = projections.orthographic(
# 1.1 * data.shape[1],
# 1.1 * data.shape[2],
# 1000,
# )

view.camera.projection = projections.perspective(
# TODO: Create a helper function for this.
fov=2 * atan(data.shape[1] / 2 / 300) * 180 / pi,
near=300,
far=1_000_000, # Just need something big
)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wonder whether some documentation/comments here would be helpful - I can't think of much right now though

@gselzer gselzer changed the title WIP: feat: Camera Projection Transforms feat: Camera Projection Transforms Jul 16, 2025
Notably, this limits the type of pygfx-specific interaction available.
But we need a scenex version of this anyways - planning to implement the
beginnings of this with events
@gselzer gselzer force-pushed the camera-projection branch from f477110 to 8f91699 Compare July 17, 2025 16:32
This was referenced Jul 17, 2025
@gselzer
Copy link
Collaborator Author

gselzer commented Jul 22, 2025

  • Consider removing CameraAdaptor._snx_zoom_to_fit, in favor of mathematically computing the bounds and setting the camera transform and projection

#29 now adds this functionality

@tlambert03
Copy link
Member

definitely very enthusiastic about this change. but I think it needs to be a bit farther before a thorough review. I pulled and ran our basic_scene example, and rather than zooming on mouse wheel, it translates. And on basic_volume, mouse wheel gave me:

/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:116: RuntimeWarning: divide by zero encountered in divide
  vectors = vectors[:-1] / vectors[-1]
/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:116: RuntimeWarning: invalid value encountered in divide
  vectors = vectors[:-1] / vectors[-1]
/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:114: RuntimeWarning: invalid value encountered in matmul
  vectors = matrix @ vectors

and then the image disappeared.

I do know that wheel events are not in the scope of this PR (and I don't think they should be)... but do you think it would be possible to get this to a point where the new model code is there and waiting to be used, but without losing functionality of our basic examples? Or is this just too big of a change and too precarious of a middle point to expect functioning mouse wheel events after this?

@gselzer
Copy link
Collaborator Author

gselzer commented Jul 28, 2025

but do you think it would be possible to get this to a point where the new model code is there and waiting to be used, but without losing functionality of our basic examples? Or is this just too big of a change and too precarious of a middle point to expect functioning mouse wheel events after this?

It is definitely a precarious middle point, yeah - both pygfx and vispy's event systems are naturally heavily related to their concepts of camera projections. For example, for orthographic projections you basically need a pygfx.PanZoomController while for perspective transforms you have other options. It's hard to ensure one (the events) continues to work when the other (camera projections) is something we are hooking into. Lots of room for bugs, like what you see:

And on basic_volume, mouse wheel gave me:

/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:116: RuntimeWarning: divide by zero encountered in divide
  vectors = vectors[:-1] / vectors[-1]
/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:116: RuntimeWarning: invalid value encountered in divide
  vectors = vectors[:-1] / vectors[-1]
/Users/talley/dev/self/scenex/.venv/lib/python3.13/site-packages/pylinalg/vector.py:114: RuntimeWarning: invalid value encountered in matmul
  vectors = matrix @ vectors

Without the Camera.type model parameter, we lost the main indicator for selecting a pygfx controller. I had arbitrarily kept the PanZoomController, which expects an orthographic projection matrix. This bug results from the mismatch.

I did spend an hour or two looking attempting a fix, but it really seems like wasted time with #28 hot on the heels of this PR (which avoids both of the errors you sae). For now, I'm thinking we just quickly patch up basic_volume.py to avoid errors?

@tlambert03
Copy link
Member

I did spend an hour or two looking attempting a fix, but it really seems like wasted time with #28 hot on the heels of this PR (which avoids both of the errors you sae).

do you have a version that includes #28 that does work with mouse events? or would we be entering a period where the demos no longer work and we're not immediately sure when they will start working again?

@gselzer
Copy link
Collaborator Author

gselzer commented Jul 28, 2025

do you have a version that includes #28 that does work with mouse events? or would we be entering a period where the demos no longer work and we're not immediately sure when they will start working again?

Yep, #28 actually already includes this PR in the commit history, so you can just run the examples on that branch. Note that there's only a pan/zoom camera event filter right now, but it's currently enabled on both basic_scene.py and basic_volume.py

@gselzer
Copy link
Collaborator Author

gselzer commented Jul 28, 2025

Hmm, does actually look like the examples on this branch do not work right now with vispy...looking into that

@gselzer
Copy link
Collaborator Author

gselzer commented Jul 28, 2025

Okay, vispy's breakages are due to problems with _snx_zoom_to_fit. As a part of this PR, I switched the vispy CameraAdaptor's underlying node to a vispy.scene.BaseCamera, which inherently does basically nothing when you call set_range on it.

Essentially, this is another case where the current breakage is actually solved in the "correct" way, with #29. As that changeset is also built atop these commits, I do not see the same errors when running our examples on that branch.

It may be worth merging that PR into this one...

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

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants