Monday, April 9, 2012

Chapter One: Drawing a Triangle

I spent nearly two weeks learning Opengl, and there were several reasons for this. The first was that I wanted to skim over easy to understand source code, and that was my fault. The second was that opengl tutorials are scattered. You can find great tutorials for one specific thing if you look hard enough, but there is not one complete, simple tutorial on Opengl. I aim to fix this. These tutorials will walk you through the basics of shaders, matrices, and 3d. I will aim to give you easy to read, well commented code on Opengl 2.0 and up. These tutorials will use java with lwjgl, but can be easily adapted for any other language with opengl bindings. And so, without further ado, I give you the first source code sample, Helper.java:


import static org.lwjgl.opengl.GL15.glGenBuffers;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import org.lwjgl.BufferUtils;
import org.lwjgl.opengl.GL15;
import org.lwjgl.util.vector.Matrix4f;

public class Helper {
 //public static final Helper instance = new Helper();
 public static IntBuffer makeIntBuffer(int[] ibuf) {
  IntBuffer buf = BufferUtils.createIntBuffer(ibuf.length);
  for(int i:ibuf) {
   buf.put(i);
  }
  buf.position(0);
  return buf;
 }
 public static FloatBuffer makeFloatBuffer(float[] ibuf) {
  FloatBuffer buf = BufferUtils.createFloatBuffer(ibuf.length);
  for(float i:ibuf) {
   buf.put(i);
  }
  buf.position(0);
  return buf;
 }
 public static FloatBuffer makeFloatBuffer(Matrix4f mat) {
  float[] f = {mat.m00,mat.m01,mat.m02,mat.m03,
      mat.m10,mat.m11,mat.m12,mat.m13,
      mat.m20,mat.m21,mat.m22,mat.m23,
      mat.m30,mat.m31,mat.m32,mat.m33};
  return makeFloatBuffer(f);
 }
 public static int makeBuffer(int target,IntBuffer bufferdata) {
  IntBuffer bufferid = BufferUtils.createIntBuffer(1);//IntBuffer.allocate(1);
  GL15.glGenBuffers(bufferid);
  int buffer = bufferid.get(0);
     GL15.glBindBuffer(target, buffer);
     GL15.glBufferData(target, bufferdata, GL15.GL_STATIC_DRAW);
     GL15.glBindBuffer(target, 0);
  return buffer;
 }
 public static int makeBuffer(int target,FloatBuffer bufferdata) {
  IntBuffer bufferid = BufferUtils.createIntBuffer(1);
  glGenBuffers(bufferid);
  int buffer = bufferid.get(0);
     GL15.glBindBuffer(target, buffer);
     GL15.glBufferData(target, bufferdata, GL15.GL_STATIC_DRAW);
     GL15.glBindBuffer(target, 0);
  return buffer;
 }
}

That was a lot of code right? This code defines a helper class with four useful methods. The first two turn an array into a buffer of the respective type, and the second two turn these into buffer ids that Opengl can use. To use these functions in you code simply use Helper.functionname(). Lets ignore the first two functions for now and focus on the makeBuffer functions. Basicly what this code does is it makes an intbuffer to hold the buffer id, generates an id, and then takes the id out of the buffer and into an int. Next it takes a target(more on that later) and the id we just created, and makes it the current buffer. It then puts the bufferdata specified by either an IntBuffer or a FloatBuffer into the current buffer. The last lines simply deactivate the buffer and return that buffer's id.

Now I know what your wondering. "This is all well and good, but how do I use this. Get on with it." And so I will. Here is how you use the functions in Helper.java:

int[] indices = {//indices go in here};
float[] vertices = {//vertices go in here};

FloatBuffer vbuf = Helper.makeFloatBuffer(vertices);
IntBuffer ibuf = Helper.makeFloatBuffer(indices);
int vid = Helper.makeBuffer(GL15.GL_ARRAY_BUFFER,vbuf);
int iid = Helper.makeBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER,ibuf);

Simple enough isn't it. Here is what we do here. We make an array for indices and vertices, we turn those arrays into buffers, and we get buffer ids from from makeBuffer. But here is the question. What are indices and vertices? It's simple really. A vertex is a point in 2d(or 3d) space. In our case we put them in pairs in the buffer. A index refers to one of these pairs. Index 0 is the first pair, index 1 is the second etc. In Opengl vertices are stored in GL_ARRAY_BUFFER, while indices are stored in GL_ELEMENT_ARRAY_BUFFER.

Next up is shaders. You may have heard of these. Shaders are what makes Opengl 2.* different from  1.*. 1.* had shaders in the form of a plugin, but in this post I will discuss the use of the official Opengl 2.* and greater shaders. First things first, a class:

import static org.lwjgl.opengl.GL20.*;
import org.lwjgl.opengl.GL20;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.nio.IntBuffer;

import org.lwjgl.BufferUtils;
import org.obsgolem.managers.ShaderManager;

public class ShaderProgram {
 int programf = 0,programv = 0,programs,attrib,uni;
 String fileb;
 public int uni2;
 public int uni3;
 
 public void createShaderProgram(String filename, ShaderManager m) {
  createShader(filename,GL_FRAGMENT_SHADER);
        createShader(filename,GL_VERTEX_SHADER);

        programs = glCreateProgram();

        glAttachShader(programs, programf);
        glAttachShader(programs, programv);
        glLinkProgram(programs);
        glValidateProgram(programs);
        //attrib = getAttribLocation("coord2d");
        //uni = getUniform("timer");
        //uni2 = getUniform("textures");
        //uni3 = getUniform("matrix");
        fileb = filename;
 }
 public int getUniform(String name) {
  return glGetUniformLocation(programs, name);
 }
 public int getAttribLocation(String aname) {
  return GL20.glGetAttribLocation(programs, aname);
 }
 public void createShader(String filename,int type) {
  String code = "";
  int id = glCreateShader(type);
  filename = "res/"+filename;
  if (type == GL_FRAGMENT_SHADER) {
            filename += ".frag";
        } else if (type == GL_VERTEX_SHADER) {
         filename += ".vert";
        }
  try {
   code = readShader(filename);
  } catch (Exception e) {
   e.printStackTrace();
  }
  glShaderSource(id, code);
        glCompileShader(id);
        if (type == GL_FRAGMENT_SHADER) {
            programf = id;
        } else if (type == GL_VERTEX_SHADER) {
         programv = id;
        }
        printLog(id);
 }
 
 public String readShader(String filename) throws Exception {
  String code = "",line;
  BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(filename)));
  while ((line = reader.readLine()) != null) {
      code += line + "\n";
  }
  return code;
 }
 
 public void printLog(int id) {
        IntBuffer intBuffer = BufferUtils.createIntBuffer(1);
        glGetShader(id, GL_INFO_LOG_LENGTH, intBuffer);

        int length = intBuffer.get();

        if (length <= 1) {
            return;
        }

        ByteBuffer infoBuffer = BufferUtils.createByteBuffer(length);
        intBuffer.flip();

        glGetShaderInfoLog(id, intBuffer, infoBuffer);

        int actualLength = intBuffer.get();
        byte[] infoBytes = new byte[actualLength];
        infoBuffer.get(infoBytes);
        System.out.print(new String(infoBytes));
        System.out.println(glIsShader(id));
 }
 
 public void recompile(ShaderManager m) {
        dispose();
        createShaderProgram(fileb,m);
    }

    public void dispose() {
        glDeleteShader(programs);
        programs = 0;

        glDeleteProgram(programf);
        programf = 0;

        glDeleteProgram(programv);
        programv = 0;
    }
}

Lets focus on only one of these functions for now: createShader. It starts by creating a shader id. This is stored in an int called id. This id is how you will use this shader. After making a shader id, we read the shader from a file. If you have ever used java before you should be able to figure out how that works. After loading the shader source we then attach it to the shader we created earlier. This puts the data into the shader. Finally we compile the shader, store the id in a variable, and then check for errors. This is basically how you load a shader. Now lets look at createShaderProgram. Shaders are stored in pairs called programs. To make a program you must first compile a vertex shader and fragment shader(more on the differences later),create a program id, attach the shaders, link them, and then validate them. The code in createShaderProgram is organized in that order.

Now lets create a shader. There are two types of shaders: Vertex and Fragment. The Vertex shader is called every time a vertex is placed on screen. The Fragment shader is called for each pixel in between each vertex. They each also have their own jobs. Vertex shaders handle the position of the vertex, while Fragment shaders handle the color of each pixel. Here is an example of a Vertex shader:

t2.vert
#version 120
attribute vec2 coord2d;
void main(void) {
  gl_Position = vec4(coord2d, 0.0, 1.0);
}
Basicly this takes an attribute(more on those later), and sets the position of the vertex to that attribute. Now for the fragment:

t2.frag
#version 120
void main(void) {
  gl_FragColor[0] = 0.0;
  gl_FragColor[1] = 0.0;
  gl_FragColor[2] = 1.0;
}
This sets the color to blue.

Now lets put this together to create a shader.

ShaderProgram pro = new ShaderProgram();
pro.createShaderProgram("t2");
glUseProgram(pro.programs);
int attid = pro.getAttribLocation("coord2d");

The last two lines make the shader active, and get the location of the attribute named "coord2d". To see how the attribute id is gotten look in getAttribLocation. This is simple enough, you just pass in the program id and the name of the attribute and you get an attribute id. Next up is actually drawing something to the screen. Here we are, the code that actually draws something:


GL11.glClearColor(1, 1, 1, 1);
     GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
     GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vid);
  //put the vertices into shader variable referenced by coord
  glVertexAttribPointer(
         attid,
         2,
         GL11.GL_FLOAT,
         false,
         0,
         0
  );
  //enable above code
  glEnableVertexAttribArray(attid);
  //bind the indices to the element array buffer
  GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, iid);
  //draw elements in range 0-indices.size()
     GL12.glDrawRangeElements(
         GL11.GL_TRIANGLE_STRIP,
         0,3,3,                  
         GL11.GL_UNSIGNED_INT,
         0           
     );
     //disable vertex attributes
     glDisableVertexAttribArray(attid);
     //unbind array buffers
     GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
     GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);

This is quite the rich chunk of code. First thing is we set the bg color, then clears to that color. The line after that makes vid the active array buffer. This allows the next line to pass the vertices to the shader. The arguments are as follows: The first is the id of the attribute we'll be putting the vertices into, the second tells the shader whether we will be passing in set of 1d, 2d, or 3d coordinates. After that comes the type of the variable, whether or not to normalize it, and two variable which I will not be using in this tutorial. Just pass in 0 for those. After that we tell it that yes, we do want to pass that attribute to the shaders. We then bind the indices to the Element array buffer, and call glDrawRangeElements. We pass into it the drawing mode, the indices we want to draw(in this case all three), the type of the indices which is int, and 0. After all this we unbind everything. Here is the entire main.java:


import static org.lwjgl.opengl.GL20.*;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.GL11;
import org.lwjgl.opengl.GL12;
import org.lwjgl.opengl.GL15;

public class Main {
 public int vid,iid,attid;
 public boolean done;
    public static void main(String args[]) {
        Main l10 = new Main();
        l10.run();
    }
    public void run() {
        try {
            init();
            while (!Display.isCloseRequested()) {
                mainloop();
                render();
                Display.update();
            }
            cleanup();
        }
        catch (Exception e) {
            e.printStackTrace();
            System.exit(0);
        }
    }
    private void mainloop() {
        
    }

    private void render() {
     GL11.glClearColor(1, 1, 1, 1);
     GL11.glClear(GL11.GL_COLOR_BUFFER_BIT);
     GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, vid);
  //put the vertices into shader variable referenced by coord
  glVertexAttribPointer(
         attid,
         2,
         GL11.GL_FLOAT,
         false,
         0,
         0
  );
  //enable above code
  glEnableVertexAttribArray(attid);
  //bind the indices to the element array buffer
  GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, iid);
  //draw elements in range 0-indices.size()
     GL12.glDrawRangeElements(
         GL11.GL_TRIANGLE_STRIP,
         0,3,3,                  
         GL11.GL_UNSIGNED_INT,
         0           
     );
     //disable vertex attributes
     glDisableVertexAttribArray(attid);
     //unbind array buffers
     GL15.glBindBuffer(GL15.GL_ARRAY_BUFFER, 0);
     GL15.glBindBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER, 0);
        //Display.update();
    }
    private void createWindow() throws Exception {
     Display.setDisplayMode(new DisplayMode(800,600));
        Display.create();
    }
    private void init() throws Exception {
        createWindow();
        initGL();
    }
    private void initGL() {
     int[] indices = {0,1,2};//indices go in here
  float[] vertices = {0.0f,  0.8f,
         -0.8f, -0.8f,
          0.8f, -0.8f
          };//vertices go in here
  FloatBuffer vbuf = Helper.makeFloatBuffer(vertices);
  IntBuffer ibuf = Helper.makeIntBuffer(indices);
  vid = Helper.makeBuffer(GL15.GL_ARRAY_BUFFER,vbuf);
  iid = Helper.makeBuffer(GL15.GL_ELEMENT_ARRAY_BUFFER,ibuf);
     GL11.glViewport(0,0,Display.getWidth(),Display.getHeight());
     ShaderProgram pro = new ShaderProgram();
     pro.createShaderProgram("t2");
     glUseProgram(pro.programs);
     attid = pro.getAttribLocation("coord2d");//attid = glGetAttribLocation(pro.programs, "coord2d");
    }
    private static void cleanup() {
        Display.destroy();
    }
}
And there you have it, three classes , five files to draw a triangle. Enjoy!

3 comments:

  1. Awesome Tutorial. Can't wait for more!!!

    ReplyDelete
    Replies
    1. Thanks. I learned a lot since this post, and my next post will have cleaner code.

      Delete
  2. Many thanks. I'm getting a desktop app running using the OpenGL ES 2.0 subset of OpenGL 2.0 under LWJGL. This helped a great deal.

    ReplyDelete