Having a sense of being grounded can be important in virtual reality. It can be much more comfortable to feel like you're standing (or sitting) than to be floating in space like a bodyless eyeball. So, let's add a floor to our scene.
This should be much more familiar now. We'll have a shader, model, and rendering pipeline similar to the cube. So, we'll just do it without much explanation.
The floor will use our light_shader
with a small modification and a new fragment shader.
Modify the light_vertex.shader
by adding a v_Grid
variable, as follows:
uniform mat4 u_Model; uniform mat4 u_MVP; uniform mat4 u_MVMatrix; uniform vec3 u_LightPos; attribute vec4 a_Position; attribute vec4 a_Color; attribute vec3 a_Normal; varying vec4 v_Color; varying vec3 v_Grid; const float ONE = 1.0; const float COEFF = 0.00001; void main() { v_Grid = vec3(u_Model * a_Position); vec3 modelViewVertex = vec3(u_MVMatrix * a_Position); vec3 modelViewNormal = vec3(u_MVMatrix * vec4(a_Normal, 0.0)); float distance = length(u_LightPos - modelViewVertex); vec3 lightVector = normalize(u_LightPos - modelViewVertex); float diffuse = max(dot(modelViewNormal, lightVector), 0.5); diffuse = diffuse * (ONE / (ONE + (COEFF * distance * distance))); v_Color = a_Color * diffuse; gl_Position = u_MVP * a_Position; }
Create a new shader in app/res/raw
named grid_fragment.shader
, as follows:
precision mediump float; varying vec4 v_Color; varying vec3 v_Grid; void main() { float depth = gl_FragCoord.z / gl_FragCoord.w; // Calculate world-space distance. if ((mod(abs(v_Grid.x), 10.0) < 0.1) || (mod(abs(v_Grid.z), 10.0) < 0.1)) { gl_FragColor = max(0.0, (90.0-depth) / 90.0) * vec4(1.0, 1.0, 1.0, 1.0) + min(1.0, depth / 90.0) * v_Color; } else { gl_FragColor = v_Color; } }
It may seem complicated, but all that we are doing is drawing some grid lines on a solid color shader. The if
statement will detect whether we are within 0.1 units of a multiple of 10. If so, we draw a color that is somewhere between white (1, 1, 1, 1) and v_Color
, based on the depth of that pixel, or its distance from the camera. gl_FragCoord
is a built-in value that gives us the position of the pixel that we are rendering in window space as well as the value in the depth buffer (z
), which will be within the range [0, 1]. The fourth parameter, w
, is essentially the inverse of the camera's draw distance and, when combined with the depth value, gives the world-space depth of the pixel. The v_Grid
variable has actually given us access to the world-space position of the current pixel, based on the local vertex position and the model matrix that we introduced in the vertex shader.
In MainActivity
, add a variable for the new fragment shader:
// Rendering variables private int gridFragmentShader;
Compile the shader in the compileShaders
method, as follows:
gridFragmentShader = loadShader(GLES20.GL_FRAGMENT_SHADER, R.raw.grid_fragment);
Create a new Java file named Floor
in the project. Add the floor plane coordinates, normals, and colors:
public static final float[] FLOOR_COORDS = new float[] { 200f, 0, -200f, -200f, 0, -200f, -200f, 0, 200f, 200f, 0, -200f, -200f, 0, 200f, 200f, 0, 200f, }; public static final float[] FLOOR_NORMALS = new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, }; public static final float[] FLOOR_COLORS = new float[] { 0.0f, 0.34f, 0.90f, 1.0f, 0.0f, 0.34f, 0.90f, 1.0f, 0.0f, 0.34f, 0.90f, 1.0f, 0.0f, 0.34f, 0.90f, 1.0f, 0.0f, 0.34f, 0.90f, 1.0f, 0.0f, 0.34f, 0.90f, 1.0f, };
Add all the variables that we need to MainActivity
:
// Model variables private static float floorCoords[] = Floor.FLOOR_COORDS; private static float floorColors[] = Floor.FLOOR_COLORS; private static float floorNormals[] = Floor.FLOOR_NORMALS; private final int floorVertexCount = floorCoords.length / COORDS_PER_VERTEX; private float[] floorTransform; private float floorDepth = 20f; // Viewing variables private float[] floorView; // Rendering variables private int gridFragmentShader; private FloatBuffer floorVerticesBuffer; private FloatBuffer floorColorsBuffer; private FloatBuffer floorNormalsBuffer; private int floorProgram; private int floorPositionParam; private int floorColorParam; private int floorMVPMatrixParam; private int floorNormalParam; private int floorModelParam; private int floorModelViewParam; private int floorLightPosParam;
Add a call to
prepareRenderingFloor
in onSufraceCreated
, which we'll write as follows:
prepareRenderingFloor();
Set up the
floorTransform
matrix in the initializeScene
method:
// Position the floor Matrix.setIdentityM(floorTransform, 0); Matrix.translateM(floorTransform, 0, 0, -floorDepth, 0);
Here's the complete prepareRenderingFloor
method:
private void prepareRenderingFloor() { // Allocate buffers ByteBuffer bb = ByteBuffer.allocateDirect(floorCoords.length * 4); bb.order(ByteOrder.nativeOrder()); floorVerticesBuffer = bb.asFloatBuffer(); floorVerticesBuffer.put(floorCoords); floorVerticesBuffer.position(0); ByteBuffer bbColors = ByteBuffer.allocateDirect(floorColors.length * 4); bbColors.order(ByteOrder.nativeOrder()); floorColorsBuffer = bbColors.asFloatBuffer(); floorColorsBuffer.put(floorColors); floorColorsBuffer.position(0); ByteBuffer bbNormals = ByteBuffer.allocateDirect(floorNormals.length * 4); bbNormals.order(ByteOrder.nativeOrder()); floorNormalsBuffer = bbNormals.asFloatBuffer(); floorNormalsBuffer.put(floorNormals); floorNormalsBuffer.position(0); // Create GL program floorProgram = GLES20.glCreateProgram(); GLES20.glAttachShader(floorProgram, lightVertexShader); GLES20.glAttachShader(floorProgram, gridFragmentShader); GLES20.glLinkProgram(floorProgram); GLES20.glUseProgram(floorProgram); // Get shader params floorPositionParam = GLES20.glGetAttribLocation(floorProgram, "a_Position"); floorNormalParam = GLES20.glGetAttribLocation(floorProgram, "a_Normal"); floorColorParam = GLES20.glGetAttribLocation(floorProgram, "a_Color"); floorModelParam = GLES20.glGetUniformLocation(floorProgram, "u_Model"); floorModelViewParam = GLES20.glGetUniformLocation(floorProgram, "u_MVMatrix"); floorMVPMatrixParam = GLES20.glGetUniformLocation(floorProgram, "u_MVP"); floorLightPosParam = GLES20.glGetUniformLocation(floorProgram, "u_LightPos"); // Enable arrays GLES20.glEnableVertexAttribArray(floorPositionParam); GLES20.glEnableVertexAttribArray(floorNormalParam); GLES20.glEnableVertexAttribArray(floorColorParam); }
Calculate MVP and draw the floor in onDrawEye
:
Matrix.multiplyMM(floorView, 0, view, 0, floorTransform, 0); Matrix.multiplyMM(modelViewProjection, 0, perspective, 0, floorView, 0); drawFloor();
Define a
drawFloor
method, as follows:
private void drawFloor() { GLES20.glUseProgram(floorProgram); GLES20.glUniform3fv(floorLightPosParam, 1, lightPosInEyeSpace, 0); GLES20.glUniformMatrix4fv(floorModelParam, 1, false, floorTransform, 0); GLES20.glUniformMatrix4fv(floorModelViewParam, 1, false, floorView, 0); GLES20.glUniformMatrix4fv(floorMVPMatrixParam, 1, false, modelViewProjection, 0); GLES20.glVertexAttribPointer(floorPositionParam, COORDS_PER_VERTEX, GLES20.GL_FLOAT, false, 0, floorVerticesBuffer); GLES20.glVertexAttribPointer(floorNormalParam, 3, GLES20.GL_FLOAT, false, 0, floorNormalsBuffer); GLES20.glVertexAttribPointer(floorColorParam, 4, GLES20.GL_FLOAT, false, 0, floorColorsBuffer); GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, floorVertexCount); }
Build and run it. It will now look like the following screenshot:
Woot!