Skip to content

Fix camera model transform bounds - #3586

Open
sankhesh wants to merge 2 commits into
Kitware:masterfrom
sankhesh:fix-camera-model-transform-bounds
Open

Fix camera model transform bounds#3586
sankhesh wants to merge 2 commits into
Kitware:masterfrom
sankhesh:fix-camera-model-transform-bounds

Conversation

@sankhesh

Copy link
Copy Markdown
Collaborator

Context

#3538 fixed the composition order so the transform is applied in world space. But the camera pose is consumed after the transform: getViewMatrix() builds lookAt(position, focalPoint, viewUp) · M, so position/focalPoint/clippingRange are interpreted in transformed space, while prop bounds are in world space. Nothing bridged the two.

This is exactly how VTK C++ behaves — vtkCamera.cxx composes ModelView = ViewTransform · ModelTransformMatrix, and vtkCamera.h documents that "Clipping distance is measured in world coordinate unless a scale factor exists in camera's ModelTransformMatrix." The difference is that C++ has vtkRenderer::ExpandBounds (vtkRenderer.cxx), which pushes bounds through the transform before deriving the camera pose from them, called from ResetCamera, ResetCameraClippingRange and ResetCameraScreenSpace. vtk-js had no equivalent.

While adding it, I found a second, unrelated bug: the model transform was being applied transposed.

Results

resetCamera / resetCameraClippingRange. Before, both derived the camera pose and clipping range from untransformed bounds. With a 10x Z exaggeration on bounds [-1000, 1000, -1000, 1000, 90, 110], resetCamera aimed the camera at world Z 100 while the rendered scene center was at 1000 — the scene was mis-framed and clipped. The error is proportional to (factor − 1) and vanishes at 1x, which is why it went unnoticed. After, the camera is aimed at the transformed center and the clipping range covers the transformed scene.

Transposed model transform. getViewMatrix composed view · Mᵀ instead of view · M. Probing a camera at (0,0,10) looking at the origin, with M = translate(1, 0, 0) from vtkTransform:

eye of world origin, actual : [0, 0, -10]
                    view · M   : [1, 0, -10]
                    view · Mᵀ  : [0, 0, -10]   <- what we were producing

A pure scale is symmetric, so M = Mᵀ and vertical exaggeration worked either way — but rotations and translations were applied transposed.

Changes

Behavior change: setModelTransformMatrix now interprets its argument as gl-matrix column-major, consistent with vtkTransform, vtkMatrixBuilder and userMatrix on vtkProp3D — and with what index.d.ts already declared (mat4). This is a silent change for anyone who passed a rotation or translation and pre-transposed to compensate; pure scales are unaffected.

  • Documentation and TypeScript definitions were updated to match those changes

PR and Code Checklist

  • semantic-release commit messages

  • Run npm run reformat to have correctly formatted code

  • This change adds or fixes unit tests

  • Tested environment:

    • vtk.js: master
    • OS: Arch Linux
    • Browser: Chromium

sankhesh added 2 commits July 27, 2026 10:51
The camera pose is consumed after modelTransformMatrix, so resetCamera and
resetCameraClippingRange must transform world-space prop bounds before
deriving the pose and clipping range from them. Ports
vtkRenderer::ExpandBounds.
modelTransformMatrix is supplied column-major like every other user-facing
matrix in vtk-js, but getViewMatrix composed it against a row-major view
matrix, yielding view * transpose(M) instead of view * M. Symmetric
matrices such as a pure scale were unaffected, so vertical exaggeration
hid the bug; rotations and translations were applied transposed.

The previous test asserted the composition in the same mixed convention as
the code, so it passed either way. It now checks where world points land in
eye coordinates.

BREAKING CHANGE: `setModelTransformMatrix` now interprets its argument as gl-matrix column-major,
consistent with `vtkTransform`, `vtkMatrixBuilder` and `userMatrix` on `vtkProp3D` — and with what
`index.d.ts` already declared (`mat4`). This is a silent change for anyone who passed a rotation or
translation and pre-transposed to compensate; pure scales are unaffected.
@sankhesh
sankhesh requested a review from finetjul July 27, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant