Skip to content

Asymmetric -2*f_x/screenWidth in cameraProjectionMatrix — origin of X-mirror unclear #35

Description

@kalwalt

Code reference: improve-viewMatrix_GL / dev.

Summary

WebARKitGL.cpp::cameraProjectionMatrix writes
projectionMatrix[0] = -2.0f * f_x / screenWidth (with a leading minus), while the symmetric Y formula
projectionMatrix[5] = 2.0f * f_y / screenHeight has no negation. The asymmetry doesn't match the standard
pinhole-to-OpenGL derivation (projectionMatrix[0] = +2 * fx / width), but in practice removing the negation
breaks AR overlay placement for a static-image example
— overlays land on the wrong screen half. So the
negation is doing real work; it just isn't obvious what X-mirror it's compensating for.

Empirical evidence

Tested in webarkit/webarkit-testing#31's static-image Teblid example (no webcam, no display mirror):

proj[0] sign Rendered position Axis rotation (THREE.AxesHelper)
-2*f_x/w (current) ✅ overlay sits on the actual marker (left panel of the printout) red X points "wrong" (toward screwdriver, off the marker)
+2*f_x/w (textbook) ❌ overlay placed on the opposite screen half (right panel, no marker) red X rotated correctly to align with the marker's horizontal

So the negation is consistent for position but creates the impression of rotation errors — and the textbook
sign does the opposite. Looks like an X-mirror is being applied somewhere and the projection's negation
compensates for it.

What I've ruled out

  • Camera intrinsics (WebARKitCamera::setupCamera)
    build a standard pinhole [fx 0 cx; 0 fy cy; 0 0 1] with the principal point at the image center. No X flip there.
  • Display side — in the static-image example, the <img> is shown with object-fit: cover and no CSS
    transform: scaleX(-1). The canvas overlay has the same layout. Both consume the image's native orientation.
  • Frame processingcontext_process.drawImage(image, 0, 0, vw, vh) does no mirror. getImageData(...) reads
    the raw canvas pixels and they go into the WASM frame buffer via HEAPU8.set at the buffer offset.
  • convert2Grayscale (WebARKitUtils.h)
    wraps the buffer in a cv::Mat of declared (rows, cols) and runs cv::cvtColor(..., COLOR_RGBA2GRAY) — no flip.
  • arglCameraViewRHf (JS path in WebARKitController.js) negates Y and Z rows only — no X negation.
  • cameraPoseFromPoints / solvePnPRansac / cv::hconcat(rMat, tvec, pose) are standard OpenCV; the
    resulting [R|t] is laid out the conventional way.

So the X mirror has to be hiding somewhere between solvePnP's output and the GL projection, and I can't pinpoint
where.

Working hypotheses (not verified)

  • Inherited artoolkit convention that paired with a horizontally mirrored webcam display, where the negation
    pre-compensates for the display flip (would explain why webcam examples look OK).
  • A subtle sign issue in how transMat/trans is read into the JS-side pose via transMatToGLMat and
    then composed with the projection.
  • A solvePnP rvec-to-rotation handedness that's offset by a 180° rotation around an unexpected axis.

What I'd like

A targeted look from someone who knows the artoolkit-derived pipeline well: where is the X-mirror that the
-2*f_x/w negation is silently compensating for? Until that's identified, the negation should stay (removing
it visibly breaks placement), but the asymmetry vs the symmetric Y formula is a real source of confusion and
blocks cleaning up the projection.

Refs

Test that locks in the current behavior

tests/webarkit_test.cc::TestCameraProjectionMatrix
already asserts projectionMatrix[0] == -1.7851850084276433. If the asymmetry is intentional, that assertion is
fine — but a comment in the source explaining why would save future readers from the same hunt.

Metadata

Metadata

Assignees

Labels

C/C++ codeconcerning the C/C++ code design and improvementsEmscriptenbugSomething isn't workingenhancementNew feature or request

Type

No fields configured for Bug.

Projects

Status
Done

Relationships

None yet

Development

No branches or pull requests

Issue actions