You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android原生Vuforia标记AR:如何让视频对象始终面向摄像头

Fixing Unstable Camera-Facing Video in Vuforia Android App

Hey there, let's dig into why your video object isn't consistently facing the camera in your Vuforia AR app. After reviewing your code, I've spotted a few issues with how you're calculating the orientation—and I've got a reliable fix for you.

The Core Problem

Your current rotation calculation relies on extracting a single value from the inverse-transposed pose matrix:

Matrix.rotateM(modelViewMatrixVideo, 0, (float) Math.toDegrees(Math.asin(-invTranspMV.getData()[6])), 0.0f, 0.f, 1.0f);

This approach only accounts for a single axis of rotation and doesn't properly handle the full 3D relationship between the tracked target and the camera. It's why the behavior is inconsistent—small changes in tracking can throw off this simplified calculation.

The Correct Approach

To make the video always face the camera, we need to:

  1. Get the camera's current pose relative to the tracked target.
  2. Calculate a model matrix that orients the video to look directly at the camera.
  3. Maintain proper matrix transformation order (translate before rotate, adjusted for Vuforia's coordinate system).

Modified renderFrame Code

Here's the updated code with fixes for consistent camera-facing behavior:

public void renderFrame(State state, float[] projectionMatrix) {
    mSampleAppRenderer.renderVideoBackground();
    GLES20.glEnable(GLES20.GL_DEPTH_TEST);
    isTracking = false;

    for (int tIdx = 0; tIdx < state.getNumTrackableResults(); tIdx++) {
        TrackableResult trackableResult = state.getTrackableResult(tIdx);
        ImageTarget imageTarget = (ImageTarget) trackableResult.getTrackable();
        
        // Only start extended tracking once when tracking initializes (not every frame)
        if (!isTracking) {
            imageTarget.startExtendedTracking();
        }
        isTracking = true;

        // Get target pose in GL matrix format
        Matrix44F targetPoseGL = Tool.convertPose2GLMatrix(trackableResult.getPose());
        float[] modelViewMatrixVideo = targetPoseGL.getData().clone();

        // Get camera's pose relative to the world, then invert to get world relative to camera
        Matrix44F cameraPoseGL = Tool.convertPose2GLMatrix(state.getCameraPose());
        Matrix44F cameraInverse = SampleMath.Matrix44FInverse(cameraPoseGL);

        // Extract camera position from inverse matrix
        float[] cameraPos = {
            cameraInverse.getData()[12],
            cameraInverse.getData()[13],
            cameraInverse.getData()[14]
        };
        // Extract target position from model view matrix
        float[] targetPos = {
            modelViewMatrixVideo[12],
            modelViewMatrixVideo[13],
            modelViewMatrixVideo[14]
        };

        // Calculate direction from target to camera
        float[] lookDir = {
            cameraPos[0] - targetPos[0],
            cameraPos[1] - targetPos[1],
            cameraPos[2] - targetPos[2]
        };
        // Normalize direction vector
        float dirLength = (float) Math.sqrt(lookDir[0]*lookDir[0] + lookDir[1]*lookDir[1] + lookDir[2]*lookDir[2]);
        if (dirLength > 0.001f) {
            lookDir[0] /= dirLength;
            lookDir[1] /= dirLength;
            lookDir[2] /= dirLength;
        }

        // Create stable rotation matrix to face camera
        float[] up = {0, 1, 0}; // Vuforia uses Y-up by default
        float[] right = new float[3];
        // Cross product to get right vector: up × lookDir
        right[0] = up[1] * lookDir[2] - up[2] * lookDir[1];
        right[1] = up[2] * lookDir[0] - up[0] * lookDir[2];
        right[2] = up[0] * lookDir[1] - up[1] * lookDir[0];
        
        // Normalize right vector
        float rightLength = (float) Math.sqrt(right[0]*right[0] + right[1]*right[1] + right[2]*right[2]);
        if (rightLength > 0.001f) {
            right[0] /= rightLength;
            right[1] /= rightLength;
            right[2] /= rightLength;
        }

        // Recalculate up vector to be perpendicular to both right and lookDir
        float[] newUp = new float[3];
        newUp[0] = lookDir[1] * right[2] - lookDir[2] * right[1];
        newUp[1] = lookDir[2] * right[0] - lookDir[0] * right[2];
        newUp[2] = lookDir[0] * right[1] - lookDir[1] * right[0];

        // Build final rotation matrix
        float[] rotationMatrix = new float[16];
        rotationMatrix[0] = right[0]; rotationMatrix[1] = newUp[0]; rotationMatrix[2] = lookDir[0]; rotationMatrix[3] = 0;
        rotationMatrix[4] = right[1]; rotationMatrix[5] = newUp[1]; rotationMatrix[6] = lookDir[1]; rotationMatrix[7] = 0;
        rotationMatrix[8] = right[2]; rotationMatrix[9] = newUp[2]; rotationMatrix[10] = lookDir[2]; rotationMatrix[11] = 0;
        rotationMatrix[12] = 0; rotationMatrix[13] = 0; rotationMatrix[14] = 0; rotationMatrix[15] = 1;

        // Apply translation first, then rotation to keep video positioned correctly
        Matrix.translateM(modelViewMatrixVideo, 0, 0f, 0f, 1f);
        Matrix.multiplyMM(modelViewMatrixVideo, 0, rotationMatrix, 0, modelViewMatrixVideo, 0);

        // Calculate final model-view-projection matrix
        float[] modelViewProjectionVideo = new float[16];
        Matrix.multiplyMM(modelViewProjectionVideo, 0, projectionMatrix, 0, modelViewMatrixVideo, 0);

        // Rendering pipeline remains unchanged
        GLES20.glEnable(GLES20.GL_BLEND);
        GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
        GLES20.glUseProgram(videoPlaybackShaderID);
        GLES20.glVertexAttribPointer(videoPlaybackVertexHandle, 3, GLES20.GL_FLOAT, false, 0, quadVertices);
        GLES20.glVertexAttribPointer(videoPlaybackTexCoordHandle, 2, GLES20.GL_FLOAT, false, 0, fillBuffer(videoQuadTextureCoordsTransformedStones));
        GLES20.glEnableVertexAttribArray(videoPlaybackVertexHandle);
        GLES20.glEnableVertexAttribArray(videoPlaybackTexCoordHandle);
        GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
        GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, videoPlaybackTextureID);
        GLES20.glUniformMatrix4fv(videoPlaybackMVPMatrixHandle, 1, false, modelViewProjectionVideo, 0);
        GLES20.glDrawElements(GLES20.GL_TRIANGLES, NUM_QUAD_INDEX, GLES20.GL_UNSIGNED_SHORT, quadIndices);
        GLES20.glDisableVertexAttribArray(videoPlaybackVertexHandle);
        GLES20.glDisableVertexAttribArray(videoPlaybackTexCoordHandle);
        GLES20.glUseProgram(0);
        GLES20.glDisable(GLES20.GL_BLEND);
        SampleUtils.checkGLError("VideoPlayback renderFrame");
    }

    GLES20.glDisable(GLES20.GL_DEPTH_TEST);
    Renderer.getInstance().end();
}

Key Improvements Explained

  • Full 3D Orientation Calculation: We calculate the complete rotation matrix using the camera's position relative to the target, ensuring the video always looks directly at the camera regardless of tracking angle.
  • Optimized Extended Tracking: We only call startExtendedTracking() once when tracking starts, avoiding unnecessary overhead and potential tracking conflicts from repeated calls.
  • Stable Coordinate System Handling: We recalculate the up vector to maintain perpendicularity with the look direction, preventing awkward tilting during camera movement.

Additional Stability Tips

  • Filter Tracking States: Add a check for trackableResult.getTrackingStatus() to only render when tracking is in TRACKED or EXTENDED_TRACKED state—this avoids rendering during unstable tracking.
  • Validate Textures: Double-check your videoQuadTextureCoordsTransformedStones to ensure they're correctly mapped; misaligned textures can mimic orientation issues even when the matrix math is correct.
  • Smooth Camera Movement: If your app allows rapid camera motion, consider adding a simple smoothing filter to the rotation matrix to reduce jitter.

Problem Effect Screenshot

问题效果示意图

内容的提问来源于stack exchange,提问作者Mirza Ahmed Baig

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:10:24