4

As a starting point I use the Vuforia (version 4) sample called MultiTargets which tracks a 3d physical "cube" in the camera feed and augments it with yellow grid lines along the cube edges. What I want to achieve is remove the textures and use diffuse lighting on the cube faces instead, by setting my own light position.

I want to do this on native Android and I do NOT want to use Unity.

It's been a hard journey of several days of work and learning. This is my first time working with OpenGL of any kind, and OpenGL ES 2.0 doesn't exactly make it easy for the beginner.

So I have a light source positioned slightly above the top face of my cube. I found that I can get the diffuse effect right if I compute the lambert factor in model space, everything remains in place regardless of my camera, and only the top face gets any light.

But when I move to using eye space, it becomes weird and the light seems to follow my camera around. Other faces get light, not only the top face. I don't understand why that is. For testing I have made sure that the light position is as expected by only using distance to lightsource for rendering pixel brightness in the fragment shader. Therefore, I'm fairly confident in the correctness of my "lightDirectionEyespace", and my only explanation is that something with the normals must be wrong. But I think I followed the explanations for creating the normal matrix correctly...

Help please!

Then there is of course the question whether those diffuse calculations SHOULD be performed in eye space? Will there be any disadvantages if I just do it in model space? I suspect that probably when I later use more models and lights and add specular and transparency, it will not work anymore, even though I don't see yet why.

My renderFrame method: (some variable names still contain "bottle", which is the object I want to light next after I get the cube right)

private void renderFrame()
{
  ShaderFactory.checkGLError("Check gl errors prior render Frame");

  // Clear color and depth buffer
  GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);

  // Get the state from Vuforia and mark the beginning of a rendering section
  final State state=Renderer.getInstance().begin();

  // Explicitly render the Video Background
  Renderer.getInstance().drawVideoBackground();

  GLES20.glEnable(GLES20.GL_DEPTH_TEST);
  GLES20.glEnable(GLES20.GL_BLEND);
  GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);

  // Did we find any trackables this frame?
  if(0 != state.getNumTrackableResults())
  {
    // Get the trackable:
    TrackableResult result=null;
    final int numResults=state.getNumTrackableResults();

    // Browse results searching for the MultiTarget
    for(int j=0; j < numResults; j++)
    {
      result=state.getTrackableResult(j);
      if(result.isOfType(MultiTargetResult.getClassType()))
        break;

      result=null;
    }

    // If it was not found exit
    if(null == result)
    {
      // Clean up and leave
      GLES20.glDisable(GLES20.GL_BLEND);
      GLES20.glDisable(GLES20.GL_DEPTH_TEST);

      Renderer.getInstance().end();
      return;
    }

    final Matrix44F modelViewMatrix_Vuforia=Tool.convertPose2GLMatrix(result.getPose());
    final float[] modelViewMatrix=modelViewMatrix_Vuforia.getData();

    final float[] modelViewProjection=new float[16];
    Matrix.scaleM(modelViewMatrix, 0, CUBE_SCALE_X, CUBE_SCALE_Y, CUBE_SCALE_Z); 
    Matrix.multiplyMM(modelViewProjection, 0, vuforiaAppSession
      .getProjectionMatrix().getData(), 0, modelViewMatrix, 0);

    GLES20.glUseProgram(bottleShaderProgramID);

    // Draw the cube:
    GLES20.glEnable(GLES20.GL_CULL_FACE);
    GLES20.glCullFace(GLES20.GL_BACK);

    GLES20.glVertexAttribPointer(vertexHandleBottle, 3, GLES20.GL_FLOAT, false, 0, cubeObject.getVertices());
    GLES20.glVertexAttribPointer(normalHandleBottle, 3, GLES20.GL_FLOAT, false, 0, cubeObject.getNormals());

    GLES20.glEnableVertexAttribArray(vertexHandleBottle);
    GLES20.glEnableVertexAttribArray(normalHandleBottle);

    // add light position and color
    final float[] lightPositionInModelSpace=new float[] {0.0f, 1.1f, 0.0f, 1.0f};
    GLES20.glUniform4f(lightPositionHandleBottle, lightPositionInModelSpace[0], lightPositionInModelSpace[1],
      lightPositionInModelSpace[2], lightPositionInModelSpace[3]);
    GLES20.glUniform3f(lightColorHandleBottle, 0.9f, 0.9f, 0.9f);

    // create the normalMatrix for lighting calculations
    final float[] normalMatrix=new float[16];
    Matrix.invertM(normalMatrix, 0, modelViewMatrix, 0);
    Matrix.transposeM(normalMatrix, 0, normalMatrix, 0);
    // pass the normalMatrix to the shader
    GLES20.glUniformMatrix4fv(normalMatrixHandleBottle, 1, false, normalMatrix, 0);

    // extract the camera position for lighting calculations (last column of matrix)
    // GLES20.glUniform3f(cameraPositionHandleBottle, normalMatrix[12], normalMatrix[13], normalMatrix[14]);

    // set material properties
    GLES20.glUniform3f(matAmbientHandleBottle, 0.0f, 0.0f, 0.0f);
    GLES20.glUniform3f(matDiffuseHandleBottle, 0.1f, 0.9f, 0.1f);

    // pass the model view matrix to the shader 
    GLES20.glUniformMatrix4fv(modelViewMatrixHandleBottle, 1, false, modelViewMatrix, 0);

    // pass the model view projection matrix to the shader
    // the "transpose" parameter must be "false" according to the spec, anything else is an error
    GLES20.glUniformMatrix4fv(mvpMatrixHandleBottle, 1, false, modelViewProjection, 0);

    GLES20.glDrawElements(GLES20.GL_TRIANGLES,
      cubeObject.getNumObjectIndex(), GLES20.GL_UNSIGNED_SHORT, cubeObject.getIndices());

    GLES20.glDisable(GLES20.GL_CULL_FACE);

    // disable the enabled arrays after everything has been rendered
    GLES20.glDisableVertexAttribArray(vertexHandleBottle);
    GLES20.glDisableVertexAttribArray(normalHandleBottle);

    ShaderFactory.checkGLError("MultiTargets renderFrame");
  }

  GLES20.glDisable(GLES20.GL_BLEND);
  GLES20.glDisable(GLES20.GL_DEPTH_TEST);

  Renderer.getInstance().end();
}

My vertex shader:

attribute vec4 vertexPosition;
attribute vec3 vertexNormal;

uniform mat4 modelViewProjectionMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 normalMatrix;

// lighting
uniform vec4 uLightPosition;
uniform vec3 uLightColor;

// material
uniform vec3 uMatAmbient;
uniform vec3 uMatDiffuse;

// pass to fragment shader
varying vec3 vNormalEyespace;
varying vec3 vVertexEyespace;
varying vec4 vLightPositionEyespace;
varying vec3 vNormal;
varying vec4 vVertex;

void main()
{
  // we can just take vec3() of a vec4 and it will take the first 3 entries
  vNormalEyespace = vec3(normalMatrix * vec4(vertexNormal, 1.0));
  vNormal = vertexNormal;
  vVertexEyespace = vec3(modelViewMatrix * vertexPosition);
  vVertex = vertexPosition;

  // light position
  vLightPositionEyespace = modelViewMatrix * uLightPosition;

  gl_Position = modelViewProjectionMatrix * vertexPosition;
}

And my fragment shader:

precision highp float; //apparently necessary to force same precision as in vertex shader

//lighting
uniform vec4 uLightPosition;
uniform vec3 uLightColor;

//material
uniform vec3 uMatAmbient;
uniform vec3 uMatDiffuse;

//from vertex shader
varying vec3 vNormalEyespace;
varying vec3 vVertexEyespace;
varying vec4 vLightPositionEyespace;
varying vec3 vNormal;
varying vec4 vVertex;

void main()
{
 vec3 normalModel = normalize(vNormal);
 vec3 normalEyespace = normalize(vNormalEyespace);
 vec3 lightDirectionModel = normalize(uLightPosition.xyz - vVertex.xyz);
 vec3 lightDirectionEyespace = normalize(vLightPositionEyespace.xyz - vVertexEyespace.xyz);

 vec3 ambientTerm = uMatAmbient;
 vec3 diffuseTerm = uMatDiffuse * uLightColor;
 // calculate the lambert factor via cosine law
 float diffuseLambert = max(dot(normalEyespace, lightDirectionEyespace), 0.0);
 // Attenuate the light based on distance.
 float distance = length(vLightPositionEyespace.xyz - vVertexEyespace.xyz);
 float diffuseLambertAttenuated = diffuseLambert * (1.0 / (1.0 + (0.01 * distance * distance)));

 diffuseTerm = diffuseLambertAttenuated * diffuseTerm;

 gl_FragColor = vec4(ambientTerm + diffuseTerm, 1.0);
}
peedee
  • 3,257
  • 3
  • 24
  • 42
  • i think there is no need to convert to eye space, you can calculate the diffuse lambret directly from normalized vectors L and N. L(light vector=lightpos-vpos) and N (normal). float diffuseLambert = max(dot(L, N), 0.0); – DanP Apr 08 '15 at 06:35
  • I just printed the normal matrix to logs to check if I see anything suspicious, and I did notice something. Early on in the renderFrame() method I scale my modelview matrix: `Matrix.scaleM(modelViewMatrix, 0, CUBE_SCALE_X, CUBE_SCALE_Y, CUBE_SCALE_Z);` and these scaling factors are in the 60-120 range. consequently, all my normalMatrix entries are in 0.0x ranges. Maybe I need to use the original modelView matrix without scaling to calculate the normalMatrix? trying that now. – peedee Apr 08 '15 at 06:58
  • The scaling that I mentioned in my last comment didn't really change anything, and come to think about it some more, that actually doesn't surprise me too much anymore. Still searching for the solution. What am I doing wrong in calculating that normalMatrix?? – peedee Apr 08 '15 at 08:36
  • I don't know the initial values for the normal vector. If its from the object (texture space?), then it is on a different space from the world or the eye space. You need to convert the normal vector to the space used by the light vector using the TBN matrix (tangent bitangent normal) – DanP Apr 08 '15 at 09:30
  • you can also try to convert the Normal to eye space usingmodel ViewMatrix. – DanP Apr 08 '15 at 09:34
  • Thanks for your inputs. I read that using modelViewMatrix to convert the normals is not sufficient because of that non-uniform scaling that I do to that matrix. For your other comment: I don't think I understand what you mean. There are no textures involved, the normals are defined together with the vertices in model space (simple cube, they are all parallel to some coordinate system axis) – peedee Apr 08 '15 at 09:46
  • one thing I'm confused about is the dimension of the normalMatrix. I found several online instructions that tell me to take the 3x3 sub-matrix of the modelViewMatrix and then invert and transpose that, but equally many tutorials don't mention the sub-matrix and just say to invert/transpose the modelViewMatrix directly. Does this make a difference? – peedee Apr 08 '15 at 09:48
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/74715/discussion-between-peedee-and-danp). – peedee Apr 08 '15 at 09:48

2 Answers2

2

I finally solved all problems. There were 2 issues that might be of interest for future readers.

  1. Vuforia CubeObject class from the official sample (current Vuforia version 4) has wrong normals. They do not all correspond with the vertex definition order. If you're using the CubeObject from the sample, make sure that the normal definitions are correctly corresponding with the faces. Vuforia fail...

  2. As suspected, my normalMatrix was wrongly built. We cannot just invert-transpose the 4x4 modelViewMatrix, we need to first extract the top left 3x3 submatrix from it and then invert-transpose that.

Here is the code that works for me:

  final Mat3 normalMatrixCube=new Mat3();
  normalMatrixCube.SetFrom4X4(modelViewMatrix);
  normalMatrixCube.invert();
  normalMatrixCube.transpose();

This code by itself is not that useful though, because it relies on a custom class Mat3 which I randomly imported from this guy because neither Android nor Vuforia seem to offer any matrix class that can invert/transpose 3x3 matrices. This really makes me question my sanity - the only code that works for such a basic problem has to rely on a custom matrix class? Maybe I'm just doing it wrong, I don't know...

peedee
  • 3,257
  • 3
  • 24
  • 42
0

thumbs up for not using the fixed functions on this! I found your example quite useful for understanding that one needs to also translate the light to a position in eyespace. All the questions i've found just recommend using glLight.

While this helped me solve using a static light source, something which is missing from your code if you wish to also make transformations on your model(s) while keeping the light source static(e.g rotating the object) is to keep track of the original modelview matrix until the view is changed, or until you're drawing another object which has a different model. So something like:

vLightPositionEyespace = fixedModelView * uLightPosition;

where fixedModelView can be updated in your renderFrame() method.

This thread on opengl discussion boards helped :)

lecker909
  • 181
  • 3
  • 6