DEV Community

Cover image for Skybox and the legend of small tasks.
Graham Long
Graham Long

Posted on

Skybox and the legend of small tasks.

The Initial Skybox attempt

I had read a few tutorials on Skyboxes, looked at source code and watched videos and decided to jump straight into coding up an implementation, suffice to say it didn't work and I had no idea why, after a while of head scratching and pulling hair out I decided that the only way forward was the way I should have done things in the first place, splitting the task into smaller tasks, initially I decided these would be:

  1. Draw a triangle (this is always my first step with OpenGL rendering).
  2. Move the triangle to the furthest viewable distance using scaling.
  3. Turn the triangle into a square.
  4. Repeat 1-3 to create the other 5 sides of the Skybox.
  5. Set each square to a different colour.
  6. replace the colour with Bitmap textures to finish the Skybox.

Step 1

My first step was to generate a Skybox class which will be responsible for rendering the Skybox, as with all my other classes this was instantiated in the MainActivities onCreate method and passed to the VrRenderer via the VrGlSurfaceView class.
The initial implementation of the Skybox class was based on my TriangleGameObject/AbstractGameObject class with a few changes:

  • As this class is instantiated in the MainActivities onCreate method the shaders and program, which require an active OpenGL context, must be compiled in a seperate method which is called in the VrRenderers onSurfaceCreated method.
  • This initialisation method takes in a distance value which will be used in step 2, This was changed to a constant in the VrRenderer so it is easier to change in one place if required in future.
  • The vertexCoordsArray for the skybox was changed from the original TriangleGameObjects values in preparation for step 3.

Step 2 & Step 3

Step 3 simply involves adding on extra vertex and the associated indices to draw the second triangle, so I combined it with step 2.
The change for step 2 involved creating a scale matrix which scaled the vertices of the skybox to 99% of the far distance.
This was done by taking the distance passed to the initialiseSkyBox method and multiplying it by 0.99f
val SCALE = (dist * 0.99f)
and then using this as the scale for the Skybox model.
Matrix.scaleM(mScaleMatrix, 0, SCALE, SCALE, SCALE)
Once this was done, there were a few initial observations:

  1. The top corners of the square had been clipped.
  2. The square did not move with the player.
  3. When the new square was in front of the triangles in the scene, the triangles were still visible, this was fixed by enabling depth testing in the VrRenderer. Observations 1 and 2 will be looked at in future steps.

Step 2 again & Step 4

Similar to step 3, Step 4 was just a case of adding more vertices and the associated indices.
To address Observation 1 in the previous Step 2 I found this tutorial which explains that, to ensure the Skybox is always rendered behind all other objects, the z component of gl_position in the vertex shader should be set equal to the w component, therefore, when perspective division is applied the z component will equal 1 and the vertex will be at the maximum distance, for this to work I needed to set the depth function in the VrRenderer to GL_LEQUAL as the skybox is Equal to the maximum depth and therefore would not be rendered by the default GL_LESS.
To address Observation 2, I modified the GameCamera class to include getters for the look direction values and used these new functions to create a skyboxViewMatrix which did not include the player translation, this was then passed to the Skybox's draw method.

Step 5

This step was added simply to see each of the squares individually and I did not really complete this as it would have meant duplicating vertices to generate clean lines for each square.

Step 6

This was the big step and went surprisingly smoothly, I added 6 bitmaps which I generated on Paint (Please do not judge these too harshly) to the projects resource folder and then modified the SkyBox class to take these as constructor arguments.
Flattened skybox images
During the development of this step I decided to create a new class TextureData which held the width, height and a ByteBuffer containing the pixel RGB values of a bitmap image.
An array mTextureData of 6 of these TextureData objects was initialised in the SkyBoxs init function, with each one representing one of the sky box bitmaps.
The biggest coding challenge of this was to extract the pixel data from the bitmap, to do this I created a getPixelData function which took a Bitmap and returned a ByteBuffer.
The getPixelData function first allocated memory for the byte buffer based on the size of the passed bitmap.
val bb = ByteBuffer.allocateDirect(bmp.height * bmp.width * BYTES_PER_PIXEL)
and then looped through every pixel position, calling val pixel = bmp.getPixel(x, y) to get the value of each pixel.
the Red, Green and Blue components of the pixel were then extracted from pixel using Color.red(pixel), Color.blue(pixel) and Color.green(pixel) each of these values was cast to a Byte using .toByte() and put into the newly created bb Byte Buffer.
The initialiseSkyBox method was updated to generate the skybox texture, set the 6 images and parameters as detailed in the tutorial above and this one as well.
Finally the shaders were modified, The vertex shader was changed to pass the position.xyz values to the fragment shader and the fragment shader was changed to set gl_FragColor to a textureCube with a samplerCube and the position.xyz values passed to it.
The samplerCube was then set by calling GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, mTextureBuffer[0]) in the draw function.

Summary

This task took a lot longer than expected, between life taking over and trying to do too much at the start, as a result of this I have started using Trello to organise my thoughts and plans for the future of this project.
Currently the list of tasks are:

  • Investigate serialising the TextureData class rather than regenerating them each time for the textures, my original 2048x2048 pixel textures took a long time to load so I need some way of speeding this up.
  • Import .obj files to add more game objects to my scenes, I have an object file parser from a previous project which I shall probably reuse for this.
  • I have noticed that the loadShader function is defined in multiple classes and could be in it's own class and reused more easily.
  • I want to start looking at some unit testing, coming from an embedded software background, I have not done a great deal of unit testing and it is something I would love to practice.
  • I am still not sure on the graphical style of the game but I would like to implement a cell shader even if this is not the final shader.

The final code for this point in development can be found here
Hopefully the next update will not take so long.

Discussion (0)