Friday, March 2, 2012

Picky, Part II

This is a continuation of my last post, where I set up color picking for the red GLKit cube.  In this post I'm setting up color picking for the blue 2.0 shader cube.  I'm starting off right where I left off, with the changes I made in my last post already made.


I'm using the exact same strategy as last time.  Namely, re-rendering the scene off screen in response to a user tap, but with lighting turned off and a unique color set.  The implementation is a little more involved since we have to communicate with the shader, but on the other hand, we can use a lot of the same work that we have already done.


Just as a reminder, when we left off, we had the red cube hooked up to the color picker so it got smaller whenever it was clicked on.




Step One:  Modify the vertex shader


The vertex shader cannot have a hard coded diffuse light color anymore, since we have to change it based on whether we're rendering it for the screen or for picking.  We also need to be able to turn lighting on or off.  We can accomplish both of those tasks by setting up uniform variables.  Modify the Shader.vsh file so it looks like this:


attribute vec4 position;

attribute vec3 normal;


varying lowp vec4 colorVarying;


uniform mat4 modelViewProjectionMatrix;

uniform mat3 normalMatrix;

uniform vec4 color;

uniform bool lit;


void main()

{

    if (lit)

    {

        vec3 eyeNormal = normalize(normalMatrix * normal);

        vec3 lightPosition = vec3(0.0, 0.0, 1.0);

    

        float nDotVP = max(0.0, dot(eyeNormal, normalize(lightPosition)));

                 

        colorVarying = color * nDotVP;

    }

    else

    {

        colorVarying = color;

    }

    

    gl_Position = modelViewProjectionMatrix * position;

}


The first thing we had to do was declare the uniform variables color and lit.  The uniform keyword simply means these values remain the same for all the vertices passed to the shader during the function call.


Inside the main function, we wrapped the existing lighting code inside an if statement.  I'd really rather not have control logic inside my shader, but for this example, maybe it's ok.  Notice how the vec4 diffuseColor declaration went away.  Now we're using the uniform color variable in its place.  Finally, if lit is set to false, we simply assign colorVarying equal to color.



Step Two:  Connect the uniforms in the view controller code


Now that we have the vertex shader set up, we need to pass it correct values.  This involves creating and configuring the two uniform variables in our view controller.


Start by adding two new members to the uniform enum near the top of the ViewController.m file.  When you're done, it should look like this:


enum

{

    UNIFORM_MODELVIEWPROJECTION_MATRIX,

    UNIFORM_NORMAL_MATRIX,

    UNIFORM_COLOR,

    UNIFORM_LIT,

    NUM_UNIFORMS

};


Next, declare two new variables in the ViewController interface section.  I put mine right below the _normalMatrix declaration.


    GLKVector4 _color;

    GLboolean _lit;


Next, initialize the two variables you just declared in the setupGL function.  I put mine at the end after my _scale initialization from the last post.  I also initialized the color to the same diffuse color value that used to be hard coded in the vertex shader.


    _color = GLKVector4Make(0.4f, 0.4f, 1.0f, 1.0f);

    _lit = true;


Now, in the loadShaders function, add the code to get the locations of the two new uniforms we added:


    uniforms[UNIFORM_COLOR] = glGetUniformLocation(_program, "color");

    uniforms[UNIFORM_LIT] = glGetUniformLocation(_program, "lit");


This code goes right after the two glGetUniformLocation calls that already exist near the end of the function.   We are simply getting the uniform IDs from OpenGL and storing them in our uniforms array for later use.


Finally, in the renderGL function we wrote in the last post, add the following code right below the two glUniformMatrix* calls that are already there.


    glUniform4fv(uniforms[UNIFORM_COLOR], 1, _color.v);

    glUniform1i(uniforms[UNIFORM_LIT], _lit);


This is the code that actually assigns the values we put in _color and _lit with the uniform variables we declare and use in the vertex shader.


As a sanity check, it's a good idea to run your program in the simulator at this point.  It should behave the same as it did at the beginning of this post.



Step Three:  Picking


Now that we've added the ability to change the blue cube's color, we need to take advantage of it.


Go down to the action handler we created in the last post.  Add the following code right before the call to [self renderGL]:


    // set the shader uniform color and disable lighting

    GLKVector4 oldColor = _color;

    _color = GLKVector4Make(1.0f, 0.0f, 0.0f, 1.0f);

    _lit = false;


Conceptually, we're doing the exact same thing we did we the red cube: we're changing the color and we're turning off lighting.  The only difference here is we need to store the old color so we can change it back when we're done.


If you run the program now in the simulator you should see the blue cube turn bright red when you click on the screen:



Now let's go ahead and change the color and lighting state back to the way it was.  You can add this code right after the [self renderGL] call:


    // change the shader color and lighting back to normal

    _color = oldColor;

    _lit = true;


Run the program in the simulator again.  The blue cube should remain visually unchanged, but the console output should indicate a bright red color when you click on the blue cube:


2012-03-02 22:43:57.792 Picky[7308:10103] 255, 0, 0, 255



Step Four:  Reaction!


Just as before, I set the blue cube up to get smaller when you click on it.  To see it happen in your program, just follow the steps from the last post.


First, create a new variable right next to your _scale variable from last time:


    float _shadeScale;


Next, initialize the variable to 1 in the setupGL function:


_shadeScale = 1.0f;


Next, add another scaling transformation to the update function.  This one goes right after the GLKMatrix4Rotate function call in the ES2 model view matrix section:


modelViewMatrix = GLKMatrix4Scale(modelViewMatrix, _shadeScale, _shadeScale, _shadeScale);


Finally, in your tap action function, add an else if statement to your previous if statement to check for the bright red color and modify the _shadeScale variable:


    else if (pixel[0] == 255 && pixel[1] == 0 && pixel[2] == 0 && pixel[3] == 255)

        _shadeScale = _shadeScale / 1.5f;


Now run the program in the simulator.  Clicking on either cube causes that cube to shrink.



Step Five:  Troubleshooting


Using shader uniform variables is a multistep process, which means it's easy to get something wrong.  In this section I purposely introduce various bugs to repo some common error states.


In the vertex shader file, I replaced all instances of the color variable with _color.  This compiled and ran with no shader compile errors in the log, but since the ViewController.m file still refers to it as "color" in this line in loadShaders, we never actually succeed in passing the color to the shader, and so the cube appears black.


uniforms[UNIFORM_COLOR] = glGetUniformLocation(_program, "color");


When I stepped through this line of code in the debugger, I saw that the glGetUniformLocation call returned -1, indicating an error.


When I commented out the declaration of the color variable in the Shader.vsh file, but left references to it, I got the following messages in the error log:


//uniform vec4 color;


2012-03-07 20:18:43.563 Picky[568:10103] Shader compile log:

ERROR: 0:28: Use of undeclared identifier 'color'

ERROR: 0:32: Use of undeclared identifier 'color'

2012-03-07 20:18:43.570 Picky[568:10103] Failed to compile vertex shader


With this compile error, the blue cube does not show up at all.  The loadShaders function returns when it detects a shader compile error, and does not even get to the set of glGetUniformLocation calls.


Another way to get a black cube is to mess up the call to glUniform4fv in the renderGL function.  Here's how it's supposed to look:


glUniform4fv(uniforms[UNIFORM_COLOR], 1, _color.v);


The v at the end of the function name indicates that we are passing a vector.  The f indicates that the vector contains float values.  The 4 indicates that it is 4 dimensional.  Using 3 instead of 4 results in a black cube.  


The first parameter is the id we use to refer to the shader uniform.  We got this id from OpenGL when we loaded the shaders.  If we pass in an incorrect id, we get a black cube. 


The second parameter is the number of vectors we are sending.  It is not the number of bytes or the dimensions in the vector, so '1' is the correct value here.  Any other value gets you a black cube.  


Calling glGetError() after any one of these incorrect function calls returns error code 1282, which means invalid operation.


Conclusion


As I said in my previous post, color picking is fine for such a simple scene, but for more complex scenes, other algorithms such as ray-casting may be more effective.


As always, please let me know if you find any mistakes in my code, or if you know of a better or easier way to solve this problem.  Also, use this code at your own risk.

1 comment:

  1. Great article, explained very clearly and very well..

    I don't suppose you would like to do one using ray casting as an alternative option, using the same style ... would be great!

    ReplyDelete