Posts
Search
Contact
Cookies
About
RSS

LWJGL a more detailed but simplified example

Added 5 Jan 2016, 10:37 a.m. edited 18 Jun 2023, 7:58 p.m.
The one big disadvantage of the previous example was although it was comprehensive there's really far too much going on to do a detail look we'd need for a tutorial. So with that in mind I stripped it right down to just the minimum. We still have all all the functionality, all the math and utilities, but the example itself is stripped down to just a textured cube and a line of text. I've put some of the code into the the Util class too as it can be either reused as is, or very easily modified - for example by looking at the GLFW api could work out easily how to add a parameter flag for full screen... Don't forget for the widest compatability and also widest selection of code to reuse, we're using GLES2.0 on Linux and automagically falling back to a subset of OpenGL2.0 where directly accessing a native GLES2.0 implementation is more problematic. In the Main class I've marked a number of sections that we'll be looking in more detail, although I'll repeat short sections here it'll be useful to have the source code to get some context (see below), and obviously the GL API is basically essential.
 /* section one */
 window = Util.createWindow(640,480);
 updateProjection(640, 480);
 
 GLFWWindowSizeCallback sizeCallback = new GLFWWindowSizeCallback() {
   @Override
   public void invoke(long window, int width, int height) {
     updateProjection( width, height);
   }
 };
 glfwSetWindowSizeCallback(window, sizeCallback);
Okay we're starting off real easy here, the window creation has been tucked away into the Util class and we'll be dealing with the updateProjection method later. All that really needs to be mentioned is that we call updateProjection any time GLFW tells us that the window has changed size, but more on that later... Section two isn't going to let you off so easy has we have the shaders, I've removed the Java string syntax so you can see the shader code a little clearer.
    /* section two */
    String fragShaderText =

      precision mediump   float;
      uniform   sampler2D u_texture;
      varying   vec2      v_frag_uv;

      void main() {
        gl_FragColor = texture2D(u_texture,v_frag_uv);
      }

    String vertShaderText =

      uniform   mat4 modelviewProjection;
      attribute vec4 pos;
      attribute vec2 uv_attrib;
      varying   vec2 v_frag_uv;
      void main() {
        gl_Position = modelviewProjection * pos;
        v_frag_uv = uv_attrib;
      }
These shaders are about as simple as they can be and still actually be useful, In later versions of the GL shader language there is an "out" variable type but for GL(ES) 2.0 we have specific variables, gl_FragColor is a vector of 4 floats which allows us to set the colour (RGB) and alpha values. I've previously touched on the differences between uniforms and varying variables, the sampler2D uniform allows us to pass a texture "name" to the shader, along with the texture2d function this allows us to lookup a colour value for a specified texture coordinate (UV). You'll notice later how the vertex and UV data are linked. Going on to the vertex shader, attributes change more frequently, where uniforms only change per shape (draw call) attributes change for each vertex. Later we'll use attribute pointers to help the shader pick through the shapes data. One thing of interest here is the varying variable this takes the UV coordinate from the uv attribute and then passes it to the fragment shader (notice that the varying in both shaders have the same name). Like gl_FragColor in the fragment shader, in the vertex shader we use gl_Position once we have calculated where the vertex should end up, the uniform matrix which is calculated in the main code to represent the model, view and projection is simply multiplied with the position attribute. Obviously I'm not going to be able to show you the whole of GLSL here, but this simple shader should be enough of a gentle introduction that you can get to grips with more complex examples with the help of a decent GLSL reference The OpenGL ES Shading Language 1.0.17 Specification is very comprehensive, but it is a bit dry so do look around for other references and try getting other shaders working but don't get too ambitious, stick with the simple stuff to start with, and once you have different shaders working, change them and see if the change does what you think it does. Section three is still dealing with the shader but its a lot more straight forward, thankfully!
    /* section three */
    int program = Util.createShaderProgram("main", vertShaderText,fragShaderText);
    glUseProgram(program);

    int attr_pos=1,attr_uv=2,u_matrix,u_texture;

    glBindAttribLocation(program, attr_pos, "pos");
    glBindAttribLocation(program, attr_uv,  "uv_attrib");

    u_matrix = glGetUniformLocation(program, "modelviewProjection");
    u_texture = glGetUniformLocation(program, "u_texture");

    glLinkProgram(program);
    if (glGetProgrami(program, GL_LINK_STATUS)==0)
          System.out.println("Shader log:\n"+glGetProgramInfoLog(program));
Compiling the actual shader program is left to a function in the Util class this just removes some fairly boiler plate code from the main class and indeed its reused in the text printing class too. Once we have our shader program we still need to get out attributes (we assign our own ID values) and also our uniforms. The names in quotes are exactly as the variables appear in the shaders. Once we have bound our attribute locations we can finally link our shader and its ready for use, we could bind our uniforms later but its so similar to binding the attributes its as well to keep it together. With the shader out of the way we can now get onto the actual vertex data
    /* section four */
    FloatBuffer vertsBuffer = ByteBuffer.allocateDirect(Shapes.boxVerts.length * 4)
                                        .order(ByteOrder.nativeOrder())
                                        .asFloatBuffer().put(Shapes.boxVerts);
    vertsBuffer.flip();

    int vbo = glGenBuffers();
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    glBufferData(GL_ARRAY_BUFFER, vertsBuffer , GL_STATIC_DRAW);
The first thing to note is that the vertex buffer needs to be directly allocated and in the machines native order, this is important for communicating any raw data blocks outside Java and especially for OpenGL. With our data in a buffer we can now tell GL where to find it, and from now on the data or VBO (Vertex Buffer Object) is referred to by its own "name" or ID (and like a texture name when you want to use it you "bind" it). When we tell GL where the data actually is we also give it a hint (GL_STATIC_DRAW) in this case we say that the data probably won't change. Once this is done, GL had copied the data for itself (and likely "uploaded" it to the GPU) and its entirely okay to free the vertsBuffer and the array used to create it if you want. In section six we get ready to actually draw something (we've skipped all the obvious stuff like program structure and initialising variables)
      /* section five */
      updateProjection(width,height);

      model.translation( 0, -0.5f, 0 );
      tmpMat.rotationY(a);
      model.multiply(tmpMat);

      mvp.set(vp);
      mvp.multiply(model);

      glBindBuffer(GL_ARRAY_BUFFER, vbo);
      glUseProgram(program);
      glBindTexture(GL_TEXTURE_2D, texture);
When positioning a model on the screen we have to take into account more than just the models position and rotation (the model matrix) which can be handled very simply with a translation and a rotation. We deal later with the View and Perspective matrix but these are pre-multiplied for us into the vp matrix, this is less handy where there is just one shape but I think you can see where there are multiple models having this VP matrix ready for us to successively multiply against different model matrices is rather useful. As well as out matrix we need to bind (use) a few bits of information, the VBO for the actual vertex data, the program ID of the shader, and also the texture. You can see how it would be easy to render the same shape but with a different texture or even shader. okay - don't panic but now we actually get to draw something! (assuming all the above is bug free of course!)
      /* section six */
      glUniformMatrix4fv(u_matrix, false , mvp.mat);

      glEnableVertexAttribArray(attr_pos);
      glEnableVertexAttribArray(attr_uv);

      glVertexAttribPointer( attr_pos, 3, GL_FLOAT, false, 20, 0 );
      glVertexAttribPointer( attr_uv, 2, GL_FLOAT, false, 20, 12 );

      glDrawArrays(GL_TRIANGLES, 0, 36);

      glDisableVertexAttribArray(attr_pos);
      glDisableVertexAttribArray(attr_uv);
glUniformMatrix4fv points the matrix uniform at the actual matrix data, don't worry about the second parameter as its for OpenGL compatibility and on GLES2.0 should always be set to false. We could have any number of vertex attributes (from multiple buffers) so its better to disable them immediately after drawing, and only enable then just as they are needed, once enabled you need to set the pointers. Note that if you change buffers but the attribute parameters are identical you still need to call glVertexAttribPointer, the first parameter identifies the attribute then say how many items per vertex there are, in this example we have just a position and UV the position has three items (X,Y and Z) where as the UV as you'd expect has just two components. Our data type is floats (GL_FLOAT) and as such it doesn't need to be normalised, next is the stride (or distance) in bytes between each vertex. lets look at an individual vertexes data
    // vert                     // UV
    -0.5f,  -0.5f,  -0.5f,      0.0f, 0.0f,
With three position and two UV components we have a total of 5 floats, each float is 4 bytes in size, giving us a stride of 20 bytes The last parameter for the UV attribute is 12 this is the offset from the start of each vertex where the UV data is stored, if we skip the position data that's three floats which times by 4 (4 bytes per float) gives us an offset of 12. finally! the draw call - this specifies in the case of our cube 36 vertices, each triangle needs 3 vertices and there are 6 sides requiring 2 triangles each 2*3*6=36 Do bare in mind that while you can have whatever arbitrary data you like along with each vertex, you might consider if it can be better encoded in additional textures. Again its worth remembering to disable the attributes if your using a different shader next. The last section we'll look at is the aforementioned updateProjection method
  /* section seven */  
  private void updateProjection( float w, float h) {
    view.lookAt(eye, centre, up);

    width = w;
    height = h;
    aspect = w / h;
    glViewport(0, 0, (int)width, (int)height);

    perspective.perspectiveProjection(45f, aspect, .1f, 30f);

    vp.set(perspective);
    vp.multiply(view);
  }
as well as updating a few matrices we also update global variables so we can keep track of the width and height should that be needed The view matrix is calculated using the eye (camera) position and the centre (point you're looking at) and finally which way is up! now if you're just moving over a 2d plane then that would always be pointing up the Y axis. This is not the only way to calculate the view matrix but it is a convenient method. The perspective projection needs first of all the field of view (here a 45 degree angle) the screens aspect ratio and also the maximum and minimum distances we want to include in our view, the distance range should be carefully chosen to give us a decent range but also minimise Z fighting, you should be also cautious of allowing things that are too close to the screen. Both the view and perspective calculations deserve far more space than I can give them here so I'd recommend looking at an article dedicated to them there is a really nice one here Now that the example is stripped down to only a little over 200 lines you should be able to get much better traction while using it as a learning aid, and don't forget to look at the almost identical but more complex example in my previous post (in this simpler example you still have all the support and utility functionality of the previous example) You can get the full source here Enjoy!