3D visionHow-to

Pan and zoom a 3D view camera from a modelview matrix

Decompose a modelview matrix into camera position and view vectors, drive pan from them, and zoom the way perspective and orthographic projections allow.

LumaSpec Vision10 min read Share on LinkedIn

On this page 10 sections
  1. 01What you need before you touch the matrix
  2. 02Step 1: own the camera state
  3. 03Step 2: split the matrix into rotation and translation
  4. 04Step 3: recover position, direction and target
  5. 05Step 4: pan by moving position and target together
  6. 06Step 5: zoom the way your projection allows
  7. 07Worked example: a 300 mm part at 500 mm
  8. 08Verify it worked
  9. 09Common mistakes and the fixes
  10. 10Questions

You have a 3D picture control rendering a point cloud, and the Setup Camera method already gives you zoom by changing the stand-off. What it lacks is scrolling: nudging the view up, down, left or right while the viewing angle stays fixed. The modelview matrix the control maintains already holds the answer — camera position, view direction and up axis packed into sixteen numbers. Below you decompose that matrix into vectors you can add to, drive pan and zoom from your own copy of the camera state, and see why the dolly that zooms a perspective view does nothing at all in an orthographic one. Every number comes from one worked example: a 300 mm part viewed from 500 mm.

What you need before you touch the matrix

Four things settle the whole job:

  • A 3D picture control exposing the Setup Camera method, plus a property that returns the current modelview matrix as a 16-element array.
  • Your projection type: perspective, the default in most 3D controls, or orthographic. Every zoom decision below hangs on that one fact.
  • One unit, end to end. Millimetres everywhere; a matrix that mixes metres and millimetres puts the camera a thousand times too far out.
  • The scene extent — the largest dimension of what you display. Here that is a 300 mm part.

The 4×4 layout is the one OpenGL uses and the same object that carries poses in camera-guided motion. Keep two frames apart while you work: the sensor's pose in the cell, and the viewing camera inside the render control. Setting up the first is covered in depth camera orientation relative to a fixed frame; this guide is entirely about the second.

The cloud you pan usually starts at a 3D sensor. A projected structured-light camera such as the MV-DPS200P-02 delivers a cloud that renders in this kind of view; a DP3000 series binocular single-line laser profiler head does the same.

3D sensorsHikrobot MV-DPS200P-02 Projected structured-light 3D cameraMV-DPS200P-02
Type
Stereo / structured light
$8,829.00 · quotedView and quote

Mounting the head over the belt is its own task; camera stands and slide stages exist so the cloud is worth looking at. Most heads also return an intensity image beside the cloud, and a separate 2D layer, if you fuse one, comes from area scan cameras.

Step 1: own the camera state

Setup Camera writes a pose; it does not read one back. So own the state instead of chasing it:

  1. Set the Camera Controller property to None. While a native controller is active, a single click in the control rewrites the pose you just set.
  2. Hold position, forward, up and stand-off L in your own variables, with start values assigned before the first render. An uninitialised state makes the first interaction jump.
  3. Write the full pose through Setup Camera after every change. Read the modelview matrix only once — to capture a pose the native controller left behind — then decompose it and take over from there.

Step 2: split the matrix into rotation and translation

A modelview matrix is a 4×4 with two working blocks:

M = [ R t ] R: 3x3 rotation — rows are the camera axes in world coordinates [ 0 1 ] t: 3x1 translation — the world-to-camera offset
  1. Read M from the control as a 16-element array, and settle the storage order once: a row-major array carries t in elements 3, 7 and 11; a column-major array carries it in 12, 13 and 14.
  2. Copy the upper-left 3×3 into R and the right-hand column into t, and drop the bottom row of 0 0 0 1.

This is the same partition a homogeneous transformation matrix uses in camera-guided motion — rotation in the corner, translation on the right — so any 4×4 tooling you already run on robot poses reuses here unchanged.

Step 3: recover position, direction and target

With a row-major array in hand (read columns instead of rows if yours is column-major):

// Illustrative variables — substitute your own names. Values in millimetres.
float[16] M                          // modelview matrix from the control, row-major
float[3]  right = {  M[0],  M[1],  M[2] }     // first row of R
float[3]  up    = {  M[4],  M[5],  M[6] }     // second row of R
float[3]  fwd   = { -M[8], -M[9], -M[10] }    // third row, negated for -Z-forward renderers
float[3]  t     = {  M[3],  M[7],  M[11] }
float[3]  pos   = { 0.0, 0.0, 0.0 }
pos[0] = -( M[0]*t[0] + M[4]*t[1] + M[8] *t[2] )
pos[1] = -( M[1]*t[0] + M[5]*t[1] + M[9] *t[2] )
pos[2] = -( M[2]*t[0] + M[6]*t[1] + M[10]*t[2] )
float     L      = 500.0              // stand-off to the scene centre
float[3]  target = pos + fwd * L
  1. Compute the three axes and the position. The negation on the third row serves renderers that look down their own negative Z axis, the common case; drop the minus signs if yours looks down +Z.
  2. Build the target the Setup Camera method wants: target = position + forward × L, with L the distance to whatever you are looking at. Any positive L sets the same view direction — the scalar only fixes how the stand-off is recorded.

The test that proves the decomposition, using the matrix from the worked example below:

// tolerance 0.001 mm
assert pos    == (0.0, -400.0, 300.0)
assert target == (0.0,    0.0,   0.0)
assert |fwd| == 1.0  and  right·up == 0.0

Step 4: pan by moving position and target together

  1. Turn the input into camera-frame millimetres: a drag of dx right and dy up becomes delta = right × dx + up × dy.
  2. Add delta to the position and to the target — both, by the same delta. Move only the position and you orbit the scene instead of scrolling past it.

Pan behaves identically under both projections: translation moves an orthographic view as readily as a perspective one. If the 3D view will end up embedded in an operator screen, mapping the same gestures on the SCADA side is what an Ignition Vision client zoom and pan implementation guide walks through.

Step 5: zoom the way your projection allows

Perspective:

  1. Hold the target fixed and shrink the stand-off: L ← L / 1.1 per wheel notch, then position = target − forward × L. Magnification is old L over new L, so the closer dolly is the bigger view, exactly as a photographer stepping forward.

Orthographic:

  1. Leave the position alone and scale the projection's view width and height by the same factor per notch — ÷1.1 to zoom in. Dollying the camera along the view direction changes nothing here: parallel rays carry no perspective divide, so projected size never depended on distance. Scale only one axis and you stretch the scene instead of zooming it.

Near and far planes clip; they do not zoom. Keep near below your closest approach minus half the scene depth, and far above your farthest approach plus it. For a 300 mm-deep scene dollied between 200 mm and 800 mm, near = 50 mm and far = 1,000 mm clip nothing.

The projection is a separate matrix from the modelview — recovering camera intrinsics from one is its own task, covered in decompose a projection matrix into camera intrinsics.

Worked example: a 300 mm part at 500 mm

A camera set 500 mm from the scene centre reports this modelview matrix:

      [  1     0     0      0  ]
M  =  [  0     0.6   0.8    0  ]     millimetres
      [  0    -0.8   0.6  -500 ]
      [  0     0     0      1  ]

R is the upper-left 3×3 and t = (0, 0, −500) mm.

Position: transpose(R) · t = (1·0 + 0·0 + 0·(−500), 0·0 + 0.6·0 + (−0.8)·(−500), 0·0 + 0.8·0 + 0.6·(−500)) = (0, 400, −300) mm, so position = (0, −400, 300) mm. The scene origin then lies sqrt(400² + 300²) = 500 mm away — the stand-off, confirmed.

Axes: right = (1, 0, 0), up = (0, 0.6, 0.8), forward = −(0, −0.8, 0.6) = (0, 0.8, −0.6). Checks: |up| = sqrt(0.36 + 0.64) = 1.00, and up · forward = 0 + 0.48 − 0.48 = 0.

Target: position + 500 × forward = (0, −400 + 400, 300 − 300) = (0, 0, 0) — the scene centre, where it belongs.

Pan 60 mm right and 40 mm up: delta = 60 × (1, 0, 0) + 40 × (0, 0.6, 0.8) = (60, 24, 32) mm. New position (60, −376, 332) mm, new target (60, 24, 32) mm; both moved by delta and the view direction is untouched.

Zoom in, perspective: L from 500 mm to 400 mm gives position = target − 400 × forward = (0, −320, 240) mm. Magnification 500 / 400 = 1.25×; with a 60° vertical field, the visible height at the target falls from 2 × 500 × tan 30° ≈ 577 mm to 2 × 400 × tan 30° ≈ 462 mm, and 577 / 462 = 1.25 — the same factor by two independent routes.

Zoom in, orthographic: view height from 600 mm to 300 mm doubles magnification to 2.0×, and dollying the position to (0, −320, 240) mm leaves every pixel where it was. Same control, same gesture, different knob.

Verify it worked

  • Dolly in one notch. The scene grows: the forward sign is right. The scene recedes: negate the third-axis vector and try again.
  • Pan once around a circle. The part should track the opposite way with no rotation; if it orbits instead, you moved the position without the target.
  • In an orthographic view, move the position along forward and confirm nothing changes; then halve the view size and confirm the part doubles.
  • Run the assert block from Step 3 against the example matrix, then against a live matrix, tolerance 0.001 mm.
  • With Camera Controller at None, click across the whole control: no jump anywhere.

If you also display where the sensor itself has travelled, the pose stream behind that is the same vector arithmetic — 3D camera trajectory visualization from pose updates covers it.

Common mistakes and the fixes

  • Round-tripping: reading the modelview, extracting the orientation, writing it back unmodified. Change your vectors and write those; the matrix is an output, not a store.
  • Treating the translation column as a zoom factor. It mixes rotation with position — rotate the camera in place and it moves while your stand-off does not.
  • Zooming with the near or far plane. They clip; a part that vanishes as you zoom in was eaten by the near plane.
  • Dollying an orthographic view. Nothing happens by construction; shrink the view size instead.
  • Leaving the native controller on. One click and your pose is gone; set it to None before the first frame.

When the view is the easy half and the station is the real job, the configurator starts at the camera step and walks the optics with these same stand-off numbers.

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

More camera-geometry guides sit on the 3D topic page, and the contact page reaches an applications engineer for the parts that resist a formula.

Questions

Why can't I read the current camera position back from Setup Camera?

Setup Camera is a write-only method: it takes position, direction and up and applies them, but it never returns the live pose. Make your own variables the source of truth — position, forward, up and stand-off, all in millimetres — and write them on every change. When you need to inherit a pose the native controller produced, read the modelview matrix once, decompose it, then set the Camera Controller property to None and take over. Leaving both active hands the operator one click that discards your pose.

Why does dollying the camera do nothing in my orthographic view?

Orthographic projection has no perspective divide: projected size never depended on distance, so moving the position along the view direction leaves every pixel where it was. Pan still works, because translation does change an orthographic view. Zoom by shrinking the projection's view width and height together — one factor for both axes. Shrinking only one axis stretches the scene instead of magnifying it.

Can the near and far planes act as a zoom?

No. They set the depth range that survives clipping; nothing is scaled. Geometry nearer than the near plane or farther than the far plane simply disappears. Size them from your dolly range: near at the closest approach minus half the scene depth, far at the farthest approach plus it. If you dolly from 200 mm out to 800 mm over a 300 mm-deep scene, that rule puts near at 50 mm and far at 1,000 mm, so nothing disappears. A part that vanishes while zooming in was eaten by the near plane.

Which part of the matrix is the view direction?

The third axis of the rotation block — the third row in row-major storage, elements M[8] to M[10], or the third column in column-major storage. Negate it for renderers that look down their negative Z axis, which most do; the example matrix yields (0, 0.8, -0.6) after negation. The ten-second check: dolly in one notch. The scene grows, the sign is right; the scene recedes, flip it.

How big should one pan step be?

Scale it to the object, not to pixels. One full drag across the control should move the view by about one object width — 300 mm on a 300 mm part — one wheel notch by about 1 percent of that, 3 mm, and one zoom notch by a factor of 1.1. Steps a hundred times the object size mean the movement values were never related to the scene.

Can the translation column of the matrix serve as a zoom factor?

No. That translation is the world-to-camera offset expressed in camera axes, so it mixes rotation with position: rotate the camera in place and the column changes while your distance to the part stays put. Distance lives along the view direction — the stand-off between position and target is your perspective zoom, and the projection's view size is the only honest orthographic factor.