在 GLES 2.0 中定位光线时混淆眼睛和世界 space

Confusing eye and world space while positioning light in GLES 2.0

我的 GLES 2.0 Android 应用程序中有关于移动点光源的问题:

我有一个人在大表面上走来走去的实例。这个人需要在头顶上方放一盏灯,以便适当地照亮他周围的一小块区域。由于灯光实例将人实例作为其父实例,因此它在世界中的位置 space 完全按照人的移动方式移动(y-Offset +4)。但是每次我启动该应用程序时,灯都不会位于顶部,并且不会完全按照人的移动方式移动(或者至少看起来不像)。尽管光线和人共享相同的 x 和 z 值,但它似乎就在人的面前。 Person 是一个立方体(还没有复杂的模型)。

这是我的立方体绘制方法代码:

public void draw(float[] pPMatrix, float[] pVMatrix)
{
    float[] MVPMatrix = new float[16];
    Matrix.setIdentityM(getParent().getModelMatrix(),0);
    Matrix.translateM(getParent().getModelMatrix(),0,mXLL, mYLL, mZLL);


    Matrix.multiplyMM(MVPMatrix, 0, pVMatrix, 0, getParent().getModelMatrix(), 0);
    Matrix.multiplyMM(MVPMatrix, 0, pPMatrix, 0, MVPMatrix, 0);

    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // ..... 

    GLES20.glUniformMatrix4fv(LightingProgram.getMVPMatrixHandle(), 1, false, MVPMatrix, 0);
    GLES20.glUniformMatrix4fv(LightingProgram.getMVMatrixHandle(), 1, false, pVMatrix, 0);

    LightObject lo = mParent.getWorld().getLightObjects().get(0);
    Matrix.multiplyMV(lo.getLightPosInEyeSpace(), 0, pVMatrix, 0, lo.getLightPosInWorldSpace(), 0 );
    GLES20.glUniform3f(LightingProgram.getLightPosHandle(), lo.getLightPosInEyeSpace()[0], lo.getLightPosInEyeSpace()[1], lo.getLightPosInEyeSpace()[2]);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexCount);

}

顶点着色器代码为:

uniform mat4 u_MVPMatrix;
uniform mat4 u_MVMatrix;        

attribute vec4 a_Position;  
attribute vec4 a_Color;         
attribute vec3 a_Normal;
attribute vec2 a_TexCoordinate;

varying vec3 v_Position;
varying vec4 v_Color;   
varying vec3 v_Normal;      
varying vec2 v_TexCoordinate;


void main()
{
    // Transform the vertex into eye space.
    v_Position = vec3(u_MVMatrix * a_Position);

    // Pass through the color.
    v_Color = a_Color;

    // Pass through the texture coordinate.
    v_TexCoordinate = a_TexCoordinate;

    // Transform the normal's orientation into eye space.
    v_Normal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));

    // gl_Position is a special variable used to store the final position.
    // Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
    gl_Position = u_MVPMatrix * a_Position;
}

片段着色器代码为:

precision mediump float;
uniform vec3 u_LightPos;   
uniform sampler2D u_Texture; 

varying vec3 v_Position;
varying vec4 v_Color;
varying vec3 v_Normal
varying vec2 v_TexCoordinate;

void main()
{

    float distance = length(u_LightPos - v_Position);

    // Get a lighting direction vector from the light to the vertex.
    vec3 lightVector = normalize(u_LightPos - v_Position);

    float diffuse = max(dot(v_Normal, lightVector), 0.0);
    diffuse = diffuse * (1.0 / (1.0 + (0.25 * distance)));

    // Add ambient lighting
    diffuse = diffuse + 0.25;

    gl_FragColor = (diffuse * v_Color * texture2D(u_Texture, v_TexCoordinate));
}

我认为这与我通过灯光对象的位置的方式有关......但我无法弄清楚什么是正确的方式。

提前致谢...:-)

==========================================

!!编辑!!我上传了一个问题的视频: https://dl.dropboxusercontent.com/u/17038392/opengl_lighting_test.mp4 (2MB)

这个场景中的每个形状都是一个立方体。当人站在房间中央时,光线对地板没有影响。如果人移动到上面的角落,灯就会移动到房间的中间。 现在这是非常奇怪的事情:因为灯位于人的上方,所以当人在房间中间时,它可以很好地照亮黄色的板条箱。这到底是怎么发生的? ;-)

==========================================

编辑 2: 好的,所以我试着按照你说的去做。但是,作为菜鸟,我很难正确地做到这一点:

任何立方体实例的绘制方法:

public void draw(float[] pPMatrix, float[] pVMatrix)
{
    float[] MVPMatrix = new float[16];
    float[] normalVMatrix = new float[16];
    float[] normalTransposed = new float[16];

    // Move object
    Matrix.setIdentityM(getParent().getModelMatrix(),0);
    Matrix.translateM(getParent().getModelMatrix(),0,mXLL, mYLL, mZLL);
    Matrix.multiplyMM(MVPMatrix, 0, pVMatrix, 0, getParent().getModelMatrix(), 0);
    Matrix.multiplyMM(MVPMatrix, 0, pPMatrix, 0, MVPMatrix, 0);

    // create normal matrix by inverting and transposing the modelmatrix
    Matrix.invertM(normalVMatrix, 0, getParent().getModelMatrix(), 0);
    Matrix.transposeM(normalTransposed, 0, normalVMatrix, 0);

    // Add program to OpenGL ES environment
    GLES20.glUseProgram(mProgram);

    // ============================
    // POSITION
    // ============================
    getVertexBuffer().position(0);
    GLES20.glVertexAttribPointer(LightingProgram.getPositionHandle(), COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, vertexStride, getVertexBuffer());
    GLES20.glEnableVertexAttribArray(LightingProgram.getPositionHandle());

    // ============================
    // COLOR
    // ============================
    getColorBuffer().position(0);
    GLES20.glVertexAttribPointer(LightingProgram.getColorHandle(), COLOR_DATA_SIZE, GLES20.GL_FLOAT, false, 0, getColorBuffer());
    GLES20.glEnableVertexAttribArray(LightingProgram.getColorHandle());

    // ============================
    // NORMALS
    // ============================
    // Pass in the normal information
    if(LightingProgram.getNormalHandle() != -1)
    {
        getNormalBuffer().position(0);
        GLES20.glVertexAttribPointer(LightingProgram.getNormalHandle(), NORMAL_DATA_SIZE, GLES20.GL_FLOAT, false, 0, getNormalBuffer());
        GLES20.glEnableVertexAttribArray(LightingProgram.getNormalHandle());
        checkGLError("normals");
    }

    // ============================
    // TEXTURE
    // ============================
    // Set the active texture unit to texture unit 0.
    GLES20.glActiveTexture(GLES20.GL_TEXTURE0);

    // Bind the texture to this unit.
    GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, getTextureHandle());

    // Tell the texture uniform sampler to use this texture in the shader by binding to texture unit 0.
    //GLES20.glUniform1i(mTextureUniformHandle, 0);
    GLES20.glUniform1i(LightingProgram.getTextureUniformHandle(), 0);


    getTextureBuffer().position(0);
    GLES20.glVertexAttribPointer(LightingProgram.getTextureCoordinateHandle(), TEXTURE_DATA_SIZE, GLES20.GL_FLOAT, false, 0, getTextureBuffer());
    GLES20.glEnableVertexAttribArray(LightingProgram.getTextureCoordinateHandle());

    // Pass the projection and view transformation to the shader
    GLES20.glUniformMatrix4fv(LightingProgram.getMVPMatrixHandle(), 1, false, MVPMatrix, 0);
    GLES20.glUniformMatrix4fv(LightingProgram.getMVMatrixHandle(), 1, false, pVMatrix, 0);
    GLES20.glUniformMatrix4fv(LightingProgram.getNormalHandle(), 1, false, normalTransposed, 0);

    LightObject lo = mParent.getWorld().getLightObjects().get(0);
    Matrix.multiplyMV(lo.getLightPosInEyeSpace(), 0, pVMatrix, 0, lo.getLightPosInWorldSpace(), 0 );
    GLES20.glUniform3f(LightingProgram.getLightPosHandle(), lo.getLightPosInEyeSpace()[0], lo.getLightPosInEyeSpace()[1], lo.getLightPosInEyeSpace()[2]);

    // Draw the triangle
    GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, mVertexCount);

    // Disable vertex array
  GLES20.glDisableVertexAttribArray(LightingProgram.getPositionHandle());
    GLES20.glDisableVertexAttribArray(LightingProgram.getTextureCoordinateHandle());
    if(LightingProgram.getNormalHandle() != -1)
        GLES20.glDisableVertexAttribArray(LightingProgram.getNormalHandle());
    GLES20.glDisableVertexAttribArray(LightingProgram.getColorHandle());
    checkGLError("end");
}

所以,我现在更新的顶点着色器代码是:

uniform mat4 u_MVPMatrix;       // A constant representing the combined model/view/projection matrix.
uniform mat4 u_MVMatrix;        // A constant representing the combined model/view matrix.
uniform mat4 u_NMatrix;         // combined normal/view matrix ???

attribute vec4 a_Position;      // Per-vertex position information we will pass in.
attribute vec4 a_Color;         // Per-vertex color information we will pass in.
attribute vec3 a_Normal;        // Per-vertex normal information we will pass in.
attribute vec2 a_TexCoordinate; // Per-vertex texture coordinate information we will pass in.

varying vec3 v_Position;        // This will be passed into the fragment shader.
varying vec4 v_Color;           // This will be passed into the fragment shader.
varying vec3 v_Normal;          // This will be passed into the fragment shader.
varying vec2 v_TexCoordinate;   // This will be passed into the fragment shader.

// The entry point for our vertex shader.
void main()
{
    // Transform the vertex into eye space.
    v_Position = vec3(u_MVMatrix * a_Position);

    // Pass through the color.
    v_Color = a_Color;

    // Pass through the texture coordinate.
    v_TexCoordinate = a_TexCoordinate;

    // Transform the normal's orientation into eye space.
    v_Normal = vec3(u_NMatrix * vec4(a_Normal, 0.0)); // THIS does not look right...
    //v_Normal = vec3(u_MVMatrix * vec4(a_Normal, 0.0));

    // gl_Position is a special variable used to store the final position.
    // Multiply the vertex by the matrix to get the final point in normalized screen coordinates.
    gl_Position = u_MVPMatrix * a_Position;
}

我想也许如果我反转并转置模型矩阵并将其保存为普通矩阵,它可能已经解决了这个问题。 但我想我完全错了..

看起来有点乱。缺少一些代码,您需要开始更频繁地注释您的代码。即使只是针对 SO 问题。

我不确定绘图输入参数是什么,但我可以假设 pPMatrix 是投影矩阵,pVMatrix 是视图矩阵。

然后在代码中有这样一条奇怪的行:Matrix.translateM(getParent().getModelMatrix(),0,mXLL, mYLL, mZLL); 我认为它会将人移动到当前位置。如果您从人的角度来看,我希望这实际上是视图矩阵的一部分。在任何情况下,此值都不会包含在您用于灯光的组件中。那么又是什么 getLightPosInWorldSpace return?

如果我们尝试将其分解一下,您会得到一个 character,其位置由他的模型矩阵定义。这描述了它在场景中的位置和方向。投影矩阵由您的视图大小和视野定义。然后根据人的方向或从您正在查看场景的任何地方计算视图矩阵(lookAt 过程是最常见的)。

无论您如何定义所有这些,光的位置都只取决于人物模型矩阵。所以需要用角色的模型矩阵乘以光照位置(0, 4, 0)。所以这可能是您想在 Matrix.multiplyMV(lo.getLightPosInEyeSpace(), 0, pVMatrix, 0, lo.getLightPosInWorldSpace(), 0 );.

中执行的操作

通过这样做,您实际上可以在 CPU 上测试灯光位置的结果是否正确,具体取决于字符位置。

现在你需要传入你的着色器(看你用什么)实际上是MVP矩阵和计算光源旁边的模型矩阵。此处不应使用 MV 矩阵,因为眼睛位置不会影响您的情况下的照明效果。

现在 v_Position 必须是在场景坐标中计算的片段,因此它必须仅与模型矩阵相乘。这基本上会为您提供场景中片段(像素)的坐标,而不是视图中的坐标。现在使用这个位置来获取与光的距离并像你已经做的那样继续计算。

那么你的法线似乎有问题。计算法线不是通过将它们与模型矩阵或模型视图矩阵相乘来完成的。想象一个场景,你有一个正常的 (0,1,0),你将它乘以一个有平移 (10, 0, 0) 的矩阵;结果法线是 (10, 1, 0) ,即使归一化没有意义,结果仍必须是 (0,1,0) 因为没有应用旋转。请研究如何生成矩阵来转换法线,其中包括所有可能的边缘情况。但请注意,您可以使用(模型)矩阵的左上角 3x3 部分来转换它们,包括大多数情况下的归一化(对于不应进行归一化的情况以及模型矩阵未按比例缩放的情况,这会失败每个轴)。

编辑:

从理论上更深入地研究您正在处理的是我们通常使用 3 个矩阵、模型、视图和投影。

投影矩阵定义屏幕上形状的投影。在您的情况下,它应该取决于您的观看比例和您想要显示的视野。它不应该影响照明、形状的位置或任何超出所有这些映射到屏幕的方式。

视图矩阵通常用于定义您如何查看场景。在你的情况下,你在哪里从哪个方向看你的场景。您可能应该为此使用一个查看程序。此矩阵不会影响照明或对象的任何位置,只会影响您在对象上的外观。

然后模型矩阵是仅用于在场景中定位特定对象的矩阵。我们使用它的原因是您可能只有 1 个顶点缓冲区用于绘制对象的所有实例。因此,在您的情况下,您有 3 个立方体,它们应该共享相同的顶点缓冲区,但在 3 个不同的地方绘制,因为它们的模型矩阵不同。

现在您的角色与场景中的任何其他对象都没有什么不同。它有一个顶点缓冲区和一个模型矩阵。如果你想从你所拥有的切换到第一人称视角,你只需要将自然基向量与角色模型矩阵相乘,然后在观察方法中使用这些向量来构建一个新的视角矩阵。基本向量可能是 location(0,0,0)forward(0,0,1)up(0,1,0)。一旦转换这些,您就可以将 "center" 构造为 location+forward。通过这样做,您在照明的工作方式或物体的照明方式上仍然没有区别,您在场景中的视图应该不会对此产生影响。

因此,您的灯光依附于由某个向量偏移的字符 offset(0,4,0)。这意味着场景中的灯光位置是与角色模型矩阵相乘的同一向量,因为该矩阵定义了角色在场景中的位置。它甚至可以解释为光位置在 (0,0,0) 并移动到一个字符位置,该位置将它与模型矩阵相乘,然后由 offset 平移,因此再次与由此创建的平移矩阵相乘向量。这很重要,因为您可以构建这个平移矩阵 T 和一个围绕 X 轴旋转的旋转矩阵 R(例如),然后将它们相乘为 modelMatrix*T*R会让光线围绕你的角色旋转。

因此,假设您拥有所有对象的所有这些矩阵,并且您有一个灯光位置,您就可以开始研究着色器了。您需要构建整个 MVP 矩阵,因为它会将对象映射到您的屏幕上。所以这个矩阵只用于gl_Position。至于场景中的实际像素,您只需将其与模型矩阵相乘即可​​。

那么第一个问题是你还需要变换法线。您需要通过反转然后转置模型矩阵来为它们构造矩阵。 The source。所以用这个矩阵而不是模型矩阵乘以你的法线。

所以现在事情变得很简单了。您已经计算了 CPU 上的灯光位置并将其作为制服发送。你有片段在现场的位置,你有它的法线。因此,您可以使用这些计算片段着色器中已有的光照。

关于视图矩阵对光照没有影响,我确实撒了点谎。它确实会影响它,但不会影响你的情况。您还没有实现任何发光,因此不会添加此照明组件。但是当(如果)你将添加它时,简单地将你的位置作为另一个制服传递然后使用视图矩阵来获得相同的结果会更容易。

很难从您发布的代码中看出什么是冲突的,但至少看起来光线位置转换不正确并且法线转换不正确。

编辑:关于调试

在使用 openGL 时,您需要在调试方面发挥创造力。看到你的结果还有很多地方可能是错误的。很难在 CPU 上或通过一些日志来检查它们,最好的方法通常是修改着色器,以便获得结果,从而提供额外的信息。

调试片段在场景中的位置:

如前所述,片段在场景中的位置不受您所看视角的影响。所以它不应该依赖于视图或投影矩阵。在您的片段着色器中,这是存储在 v_Position.

中的值

您需要设置您要测试的边界,这取决于您的场景大小(您放置墙壁和立方体的位置......)。

你说你的墙偏移了 25,所以可以安全地假设你的场景将在 [-30, 30] 范围内。您需要分别调试每个轴,例如让红色值为 -25,绿色值为 25。要测试 Y 坐标(通常是高度),您只需使用 gl_FragColor = vec4(1.0-(v_Position.y+30)/60, (v_Position.y+30)/60), 0.0, 1.0)。这应该在所有对象上显示一个漂亮的渐变,其中它们的 Y 值越低,颜色应该越红。然后你对其他 2 个组件做同样的事情,对于它们中的每一个,你应该在每个自己的方向上获得这些漂亮的渐变。渐变应该在整个场景中均等可见,而不仅仅是每个对象。

调试场景中的法线:

对象之间的法线必须一致,具体取决于它们面向的方向。由于您案例中的所有对象都平行于轴并已归一化,因此这应该非常容易。您期望只有 6 个可能的值,因此 2 遍应该可以完成工作(正和负)。

对正法线使用gl_FragColor = vec4(max(v_Normal.x, 0.0), max(v_Normal.y, 0.0), max(v_Normal.z, 0.0), 1.0)。这会将所有面向正 X 的面显示为红色,所有面向正 Y 的面都显示为绿色,所有面向正 Z 的面都显示为蓝色。

第二个测试是gl_FragColor = vec4(max(-v_Normal.x, 0.0), max(-v_Normal.y, 0.0), max(-v_Normal.z, 0.0), 1.0),它对那些面对负坐标的人做的完全一样。

调试灯位:

当第一次测试通过时,可以通过仅照亮附近的物体来测试灯光位置。所以 length(u_LightPos - v_Position.xyz) 显示了光距。您必须对其进行标准化,以便在您的场景中使用 highp float scale = 1.0 - length(u_LightPos - v_Position.xyz)/50.0,然后在颜色中使用它作为 gl_FragColor = vec4(scale, scale, scale, 1.0)。这将使光附近的所有物体都变成白色,而那些非常远的物体则变成黑色。应该显示一个漂亮的渐变。

这些测试的要点:

您这样做是因为您当前的结果取决于多个值,所有这些值都可能被窃听。通过隔离问题,您将很容易找到问题所在。

第一个测试丢弃了法线和灯光位置,所以如果它不正确,那只意味着你将你的位置与错误的矩阵相乘。计算v_Position.

时需要保证使用模型矩阵乘以位置

第二个测试丢弃场景中的位置和灯光位置。您仍然可以看到您的场景,但颜色将仅由您的法线定义。如果结果不正确,您要么开始时有错误的法线,要么在使用法线矩阵时它们被错误地转换。你甚至可以禁用正常的矩阵乘法只是为了看看两者中哪一个是不正确的。如果禁用不能解决问题,那么您的顶点缓冲区中的法线不正确。

第三个测试放弃了法线和计算光照效果的逻辑。第一个测试必须通过,因为我们仍然需要您模型的位置。因此,如果此测试失败,您很可能没有正确定位灯光。其他可能性是法线不正确(您已对其进行测试),第三种是您计算的光效不正确。