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 processing —
context_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.
Summary
WebARKitGL.cpp::cameraProjectionMatrixwritesprojectionMatrix[0] = -2.0f * f_x / screenWidth(with a leading minus), while the symmetric Y formulaprojectionMatrix[5] = 2.0f * f_y / screenHeighthas no negation. The asymmetry doesn't match the standardpinhole-to-OpenGL derivation (
projectionMatrix[0] = +2 * fx / width), but in practice removing the negationbreaks 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]signTHREE.AxesHelper)-2*f_x/w(current)+2*f_x/w(textbook)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
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.<img>is shown withobject-fit: coverand no CSStransform: scaleX(-1). The canvas overlay has the same layout. Both consume the image's native orientation.context_process.drawImage(image, 0, 0, vw, vh)does no mirror.getImageData(...)readsthe raw canvas pixels and they go into the WASM frame buffer via
HEAPU8.setat the buffer offset.convert2Grayscale(WebARKitUtils.h)wraps the buffer in a
cv::Matof declared(rows, cols)and runscv::cvtColor(..., COLOR_RGBA2GRAY)— no flip.arglCameraViewRHf(JS path inWebARKitController.js) negates Y and Z rows only — no X negation.cameraPoseFromPoints/solvePnPRansac/cv::hconcat(rMat, tvec, pose)are standard OpenCV; theresulting
[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)
pre-compensates for the display flip (would explain why webcam examples look OK).
transMat/transis read into the JS-sideposeviatransMatToGLMatandthen composed with the projection.
solvePnPrvec-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/wnegation is silently compensating for? Until that's identified, the negation should stay (removingit 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::TestCameraProjectionMatrixalready asserts
projectionMatrix[0] == -1.7851850084276433. If the asymmetry is intentional, that assertion isfine — but a comment in the source explaining why would save future readers from the same hunt.