Pictures look best in a frame. Let's add one now. There are a number of ways to accomplish this, but we are going to use shaders. The frame will also be used for the thumbnail images and will enable us to change colors to highlight when the user selects an image. Furthermore, it helps define a region of contrast, which ensures that you can see the edge of any image on any background.
We can start by writing the shader programs which, among other things, define the variables they will need from the Material
object that uses it.
If necessary, create a resource directory for the shaders, res/raw/
. Then, create the border_vertex.shader
and border_fragment.shader
files. Define them as follows.
The border_vertex
shader is identical to the unlit_tex_vertex
shader that we were using.
File: res/raw/border_vertex.shader
uniform mat4 u_MVP; attribute vec4 a_Position; attribute vec2 a_TexCoordinate; varying vec3 v_Position; varying vec2 v_TexCoordinate; void main() { // pass through the texture coordinate v_TexCoordinate = a_TexCoordinate; // final point in normalized screen coordinates gl_Position = u_MVP * a_Position; }
For the border_fragement
shader, we add variables for a border color (u_Color
) and width (u_Width
). Then, add a bit of logic to decide whether the current coordinate being rendered is on the border or in the texture image:
File: res/raw/border_fragment.shader
precision mediump float; uniform sampler2D u_Texture; varying vec3 v_Position; varying vec2 v_TexCoordinate; uniform vec4 u_Color; uniform float u_Width; void main() { // send the color from the texture straight out unless in // border area if( v_TexCoordinate.x > u_Width && v_TexCoordinate.x < 1.0 - u_Width && v_TexCoordinate.y > u_Width && v_TexCoordinate.y < 1.0 - u_Width ){ gl_FragColor = texture2D(u_Texture, v_TexCoordinate); } else { gl_FragColor = u_Color; } }
Note that this technique cuts off the edges of the image. We found this to be acceptable, but if you really want to see the entire image, you can offset the UV coordinates within the texture2D
sampler call. It would look something like this:
float scale = 1.0 / (1 - u_Width * 2); Vec2 offset = vec( v_TexCoordinate.x * scale – u_Width, v_TexCoordinate.x * scale – u_Width); gl_FragColor = texture2D(u_Texture, offset);
Finally, observant readers might notice that when the plane is scaled non-uniformly (to make it a rectangle), the border will be scaled so that the vertical borders might be thicker or thinner than the horizontal borders. There are a number of ways to fix this, but this is left as an exercise for the (over-achieving) reader.
Next, we define the material for the border shader. Create a new Java class in RenderBoxExt/materials/
named BorderMaterial
and define it as follows:
public class BorderMaterial extends Material { private static final String TAG = "bordermaterial"; }
Add material variables for the texture ID, border width, and color. Then, add variables for the shader program references and buffers, as shown in the following code:
int textureId; public float borderWidth = 0.1f; public float[] borderColor = new float[]{0, 0, 0, 1}; // black static int program = -1; //Initialize to a totally invalid value for setup state static int positionParam; static int texCoordParam; static int textureParam; static int MVPParam; static int colorParam; static int widthParam; FloatBuffer vertexBuffer; FloatBuffer texCoordBuffer; ShortBuffer indexBuffer; int numIndices;
Now we can add a constructor. As we've seen earlier, it calls a setupProgram
helper method that creates the shader program and obtains references to its parameters:
public BorderMaterial() { super(); setupProgram(); } public static void setupProgram() { //Already setup? if (program > -1) return; //Create shader program program = createProgram(R.raw.border_vertex, R.raw.border_fragment); //Get vertex attribute parameters positionParam = GLES20.glGetAttribLocation(program, "a_Position"); texCoordParam = GLES20.glGetAttribLocation(program, "a_TexCoordinate"); //Enable them (turns out this is kind of a big deal ;) GLES20.glEnableVertexAttribArray(positionParam); GLES20.glEnableVertexAttribArray(texCoordParam); //Shader-specific parameters textureParam = GLES20.glGetUniformLocation(program, "u_Texture"); MVPParam = GLES20.glGetUniformLocation(program, "u_MVP"); colorParam = GLES20.glGetUniformLocation(program, "u_Color"); widthParam = GLES20.glGetUniformLocation(program, "u_Width"); RenderBox.checkGLError("Border params"); }
Likewise, we add a setBuffers
method to be called by the RenderObject
component (Plane
):
public void setBuffers(FloatBuffer vertexBuffer, FloatBuffer texCoordBuffer, ShortBuffer indexBuffer, int numIndices){ //Associate VBO data with this instance of the material this.vertexBuffer = vertexBuffer; this.texCoordBuffer = texCoordBuffer; this.indexBuffer = indexBuffer; this.numIndices = numIndices; }
Provide a setter method for the texture ID:
public void setTexture(int textureHandle) { textureId = textureHandle; }
Add the draw code, which will be called from the Camera
component, to render the geometry prepared in the buffers (via setBuffer
). The draw method looks like this:
@Override public void draw(float[] view, float[] perspective) { GLES20.glUseProgram(program); // 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, textureId); // Tell the texture uniform sampler to use this texture in // the shader by binding to texture unit 0. GLES20.glUniform1i(textureParam, 0); Matrix.multiplyMM(modelView, 0, view, 0, RenderObject.model, 0); Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, modelView, 0); // Set the ModelViewProjection matrix for eye position. GLES20.glUniformMatrix4fv(MVPParam, 1, false, modelViewProjection, 0); GLES20.glUniform4fv(colorParam, 1, borderColor, 0); GLES20.glUniform1f(widthParam, borderWidth); //Set vertex attributes GLES20.glVertexAttribPointer(positionParam, 3, GLES20.GL_FLOAT, false, 0, vertexBuffer); GLES20.glVertexAttribPointer(texCoordParam, 2, GLES20.GL_FLOAT, false, 0, texCoordBuffer); GLES20.glDrawElements(GLES20.GL_TRIANGLES, numIndices, GLES20.GL_UNSIGNED_SHORT, indexBuffer); RenderBox.checkGLError("Border material draw"); }
One more thing; let's provide a method to destroy an existing material:
public static void destroy(){ program = -1; }
To use the BorderMaterial
class instead of the default UnlitTexMaterial
class, we wrote in the Plane
class previously, we can add it to the Plane
Java class, as follows. We plan to create the material outside the Plane
class (in MainActivity
), so we just need to set it up. In Plane.java
, add the following code:
public void setupBorderMaterial(BorderMaterial material){ this.material = material; material.setBuffers(vertexBuffer, texCoordBuffer, indexBuffer, numIndices); }
In MainActivity
, modify the setupScreen
method to use this material instead of the default one, as follows. We first create the material and set the texture to our sample image. We don’t need to set the color, which will default to black. Then we create the screen plane and set its material. And then create the transform and add the screen component:
void setupScreen() { //... Screen = new Plane(); BorderMaterial screenMaterial = new BorderMaterial(); screenMaterial.setTexture(RenderBox.loadTexture( R.drawable.sample360)); screen.setupBorderMaterial(screenMaterial); //... }