谷歌的Android OpenGL教程是否教授了错误的线性代数知识?

31

在帮助另一个用户解决关于响应触摸事件安卓教程的问题后,我下载了源代码,但看到的内容让我感到困惑。该教程似乎不能确定它想使用行向量还是列向量,对我来说看起来混乱不堪。

在安卓矩阵页面上,他们声称他们的约定是列向量/列主元,这是OpenGL的典型做法。

我是正确的,还是有什么我没注意到的地方?以下是相关部分:

通过将 mProjMatrix * mVMatrix 相乘来创建一个 MVPMatrix。到目前为止还算顺利。

    // Set the camera position (View matrix)
    Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // Calculate the projection and view transformation
    Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0)
下一步他们将一个旋转附加到MVPMatrix的左侧?这似乎有点奇怪。
    // Create a rotation for the triangle
    Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, -1.0f);

    // Combine the rotation matrix with the projection and camera view
    Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0)

以非转置的顺序上传。

    GLES20.glUniformMatrix4fv(mMVPMatrixHandle, 1, false, mvpMatrix, 0);

最后在他们的着色器中,进行了向量*矩阵乘法?

    // the matrix must be included as a modifier of gl_Position
    "  gl_Position = vPosition * uMVPMatrix;" 

将所有这些加在一起,我们得到:

gl_Position = vPosition * mRotation * mProjection * mView;

就算我尽力想也不可能觉得这是正确的。这里有什么解释我没看到的吗?


2
对我来说有两种可能性。要么这个例子是错误的,要么他们实现了矩阵操作的不同方式。请参见 - Fabien R
请问您能否澄清一下问题? - user1071136
5个回答

26

作为那个编写OpenGL教程的人,我可以确认示例代码是错误的。具体来说,着色器代码中因子的顺序应该被反转:

"  gl_Position = uMVPMatrix * vPosition;"

在使用旋转矩阵时,因为矩阵是从右往左应用的,所以旋转应该作为最后一个因子。一般的规则是,矩阵按从右到左的顺序应用,并且首先应用旋转(它是“MVP”中的“M”部分),因此它需要成为最右边的操作数。此外,您应该使用一个临时矩阵进行计算,这是建议由Ian Ni-Lewis提出的(请查看他下面更完整的回答):

float[] scratch = new float[16];
// Combine the rotation matrix with the projection and camera view
Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

感谢您关注这个问题。我会尽快修复培训课程和示例代码。

编辑:现在可下载的示例代码和OpenGL ES培训课程已经修正了这个问题,包括有关因子正确顺序的注释。感谢大家的反馈!


2
还有一个问题:Matrix.multiplyMM没有检查别名。如果您将其中一个输入参数重用为输出参数,则它将被覆盖。 - Ian Ni-Lewis
谢谢,我一直在寻找这个答案。 在void main之前应该添加uniform mat4 uMVPMatrix,否则会出现空白屏幕和ClearColor。 - ShouravBR
我发现Matrix.multiplyMM(mMVPMatrix, 0, mRotationMatrix, 0, mMVPMatrix, 0);比重新排序后的矩阵能够给出正确的旋转变换。其中,T(x) = A.xmultiplyMM(float[] result, int resultOffset, float[] lhs, int lhsOffset, float[] rhs, int rhsOffset)是相关函数。只有修复顶点着色器才对我有效。 - ShouravBR
抱歉,您认为哪个旋转是“正确”的?如果您使用Shourav的建议,您最终会得到一个扭曲的三角形,因为变换的顺序是错误的。也许您的意思是与您预期的相比,变换是“反向”的?当您将(逆时针)旋转应用于对象而不是其参考框架时,就会出现这种情况。尝试移动相机或使用较不简单的对象;很明显,如果不修复操作数的顺序,随着旋转接近90度,对象在X方向上被缩放。 - Ian Ni-Lewis

10
教程中有些错误,但是在这个非常有限的上下文环境中,很多错误要么互相抵消,要么不明显(固定摄像头位于(0,0)处,仅绕Z轴旋转)。旋转是反向的,但除此之外它看起来基本正确。(如果想知道为什么它是错的,请尝试一个更复杂的相机:例如将眼睛和观察点设置为y = 1。)
其中一个使调试变得非常困难的问题是Matrix方法不会检测其输入是否存在别名。教程代码让人觉得可以使用相同的矩阵作为输入和结果调用Matrix.multiplyMM。事实并非如此。但由于实现是逐列相乘的,如果重用右侧(如当前代码中的mMVPMatrix是rhs和结果),即使左侧被覆盖,也不太可能出现问题。每个左侧的列在相应的结果列被写入之前都会被读取,因此即使LHS被覆盖,输出也将是正确的。但是,如果右侧与结果相同,则在其第一列被完全读取之前就会被覆盖。
所以,教程代码处于一种局部最大值状态:它似乎可以工作,并且如果改变任何一件事,它会出现明显错误。这使人们相信,尽管看起来不正确,但它可能是正确的。 ;-)
无论如何,下面是一些替换代码,可以得到我认为预期结果的效果。
Java代码:
@Override
public void onDrawFrame(GL10 unused) {
    float[] scratch = new float[16];

    // Draw background color
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    // Set the camera position (View matrix)
    Matrix.setLookAtM(mVMatrix, 0, 0, 0, -3, 0f, 0f, 0f, 0f, 1.0f, 0.0f);

    // Calculate the projection and view transformation
    Matrix.multiplyMM(mMVPMatrix, 0, mProjMatrix, 0, mVMatrix, 0);

    // Draw square
    mSquare.draw(mMVPMatrix);

    // Create a rotation for the triangle
    Matrix.setRotateM(mRotationMatrix, 0, mAngle, 0, 0, 1.0f);

    // Combine the rotation matrix with the projection and camera view
    Matrix.multiplyMM(scratch, 0, mMVPMatrix, 0, mRotationMatrix, 0);

    // Draw triangle
    mTriangle.draw(scratch);
}

着色器代码:

gl_Position =  uMVPMatrix * vPosition;
注意:这些修复可以使投影正确,但也会翻转旋转方向。这是因为原始代码应用了错误的变换顺序。可以这样理解:不是将对象顺时针旋转,而是将相机逆时针旋转。当你修复操作顺序,使旋转应用于对象而不是相机时,对象开始逆时针旋转。矩阵并没有错,错的是用于创建矩阵的角度。
因此,为了获得“正确”的结果,还需要翻转mAngle的符号。

2
我是这样解决这个问题的:

我按照以下步骤进行操作:

@Override
public void onDrawFrame(GL10 unused) {      
    GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);

    Matrix.setLookAtM(mViewMatrix, 0, 0, 0, -1f, 0f, 0f, 0f, 0f, 1.0f, 0.0f);        

    Matrix.setRotateM(mModelMatrix, 0, mAngle, 0, 0, 1.0f);
    Matrix.translateM(mModelMatrix, 0, 0.4f, 0.0f, 0);

    mSquare.draw(mProjMatrix,mViewMatrix,mModelMatrix);
}

@Override
public void onSurfaceChanged(GL10 unused, int width, int height) {
    ...  
    Matrix.frustumM(mProjMatrix, 0, -ratio, ratio, -1, 1, 1, 99);

}

class Square {

    private final String vertexShaderCode =
        "uniform mat4 uPMatrix; \n" +
        "uniform mat4 uVMatrix; \n" +
        "uniform mat4 uMMatrix; \n" +

        "attribute vec4 vPosition; \n" +
        "void main() { \n" +
        "  gl_Position = uPMatrix * uVMatrix * uMMatrix * vPosition; \n" +
        "} \n";

    ...

    public void draw(float[] mpMatrix,float[] mvMatrix,float[]mmMatrix) {

        ...

        mPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uPMatrix");
        mVMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uVMatrix");
        mMMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMMatrix");

        GLES20.glUniformMatrix4fv(mPMatrixHandle, 1, false, mpMatrix, 0);
        GLES20.glUniformMatrix4fv(mVMatrixHandle, 1, false, mvMatrix, 0);
        GLES20.glUniformMatrix4fv(mMMatrixHandle, 1, false, mmMatrix, 0);

        ...
    }
}

1

我正在处理相同的问题,以下是我的发现:

我相信Joe的示例是正确的,
包括着色器代码中因子的顺序:

gl_Position = vPosition * uMVPMatrix;

要验证它,只需尝试使用反转因子顺序旋转三角形,它将把三角形拉伸到90度的消失点。

真正的问题似乎在于 setLookAtM 函数。
在乔的示例中,参数为:

Matrix.setLookAtM(mVMatrix, 0,
     0f, 0f,-3f,   0f, 0f, 0f,   0f, 1f, 0f );

这也是非常合理的。
然而,生成的视图矩阵让我感到很奇怪:

-1  0  0  0
 0  1  0  0
 0  0 -1  0
 0  0 -3  1

我们可以看到,这个矩阵将会反转X坐标, 因为第一个元素是-1,
这将导致屏幕左右翻转。
它也会反转Z顺序,但让我们在这里专注于X坐标。

我认为setLookAtM函数也是正常工作的。
然而,由于Matrix类不是OpenGL的一部分, 它可以使用其他坐标系,
例如 - 带有向下指向Y轴的常规屏幕坐标。
这只是一个猜测,我没有真正验证过。

可能的解决方案:
我们可以手动构建所需的视图矩阵,
代码如下:

Matrix.setIdentityM(mVMatrix,0);
mVMatrix[14] = -3f;

或者,我们可以尝试通过给予相反的相机坐标:0、0、+3(而不是-3),来欺骗setLookAtM函数。

Matrix.setLookAtM(mVMatrix, 0,
     0f, 0f, 3f,   0f, 0f, 0f,   0f, 1f, 0f );

生成的视图矩阵将是:

1  0  0  0
0  1  0  0
0  0  1  0
0  0 -3  1

这正是我们所需要的。
现在相机的表现符合预期,
样例也可以正常工作了。


1
你说得对,仅仅在着色器中修复操作数的顺序会产生更糟糕而不是更好的结果。但这是因为示例存在多个问题。 - Ian Ni-Lewis
我应该提到,你的解决方案实际上是我尝试的第一件事,因为我确信我看到了一个左右手问题。但即使它看起来正确,实际上并不是;尝试稍微移动和旋转相机,你很快就会明白为什么。我下面发布的代码是正确的。 - Ian Ni-Lewis

0

在尝试移动三角形时,除了以下方法,当前更新的Android示例代码中没有其他建议适用于我。

以下链接包含答案。花了一天多的时间才找到它。在这里发布以帮助其他人,因为我已经看到了这篇文章很多次。 OpenGL ES Android矩阵变换


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接