Downloading Directly From Torrentz

The popular torrent search engine, Torrentz.eu does not provide direct links to download torrents. Normally, one would have to pick one of the available torrent websites at the top of the page in order to get a .torrent file or magnet link. However, I recently noticed that all the information needed to generate a .torrent file or magnet link is actually available on the page itself, namely the info hash and a list of trackers. As such I wrote some JavaScript to generate a magnet link.

var infoHash = document.querySelector("div.trackers div").textContent.substring(11),
displayName = document.querySelector("div.download h2 span").textContent,
trackers = document.querySelectorAll("div.trackers dl a"),
a = document.createElement("a"),
magnet = "magnet:?xt=urn:btih:" + infoHash + "&dn=" + displayName;
for (var i = 0; i < trackers.length; i++) {
    magnet += "&tr=" + trackers[i].textContent;
}
console.log(magnet);
window.location = magnet;

The last line is optional and performs the equivalent of clicking on the link.

For the sake of convenience, I have also made a bookmarklet that will automatically start a torrent download when clicked (assuming that you are currently on a Torrentz.eu torrent page). Drag this button onto your bookmarks toolbar and you will be good to go:

Torrentz Download

Starting out with OpenGL 2D in Java, Part 3

Welcome, class, to Part 3! Today we will be discussing indexed vertex buffers and using them to optimize tessellation.

Indexed Vertex Buffer Objects

Rationale

In complicated shapes one often encounters several primitives which share at least one vertex. This unfortunate situation is exacerbated by tessellation, sometimes dramatically. To demonstrate this I will use the below-pictured, slightly creepy, Eye of Providence graphic. I have chosen this mainly to please my sponsors, the Illuminati, but also because as a pointy object it is bound to have a ton of reused vertices after tessellation.

Eye of Providence

It’s looking at you…

When tessellated it looks as follows.

Eye of Providence, tessellated

Possibly even creepier now…

The change may not look drastic, but it actually goes from having 82 vertices, untessellated, to having 204, post-tessellation. Luckily we can mitigate the effects of the almost 250% increase in vertex-count by using an indexed vertex buffer object.

Preparation

We will start with the skeleton of an OpenGL program that you should be quite familiar with by now, assuming you have read parts 1 and 2. This includes the defining of the display object, as well as setting up a rendering loop.

public void start() {
    try {
        Display.setDisplayMode(new DisplayMode(800, 600));
        Display.create(new PixelFormat(0, 8, 0, 0));
    } catch (LWJGLException e) {
        e.printStackTrace();
        System.exit(1);
    }

    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, 800, 0, 600, 1, -1);
    glMatrixMode(GL_MODELVIEW);

    while (!Display.isCloseRequested()) {
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        Display.update();
        Display.sync(60);
    }
}

Next you should define the shape to draw in an array, before the rendering loop starts.

double[][][] eyeOfProvidence = {
    {
        {
            399.31702, 590.00001, 0, 0.333, 0.867, 1,
            412.94567, 582.47441, 0, 0.333, 0.867, 1,
            729.94367, 32.483490000000074, 0, 0.333, 0.867, 1,
            729.99997, 17.46959000000004, 0, 0.333, 0.867, 1,
            716.9979099999999, 9.999990000000025, 0, 0.333, 0.867, 1,
            400.12413999999995, 12.332290000000057, 0, 0.333, 0.867, 1,
            399.87577999999996, 12.332290000000057, 0, 0.333, 0.867, 1,
            83.00200799999999, 9.999990000000025, 0, 0.333, 0.867, 1,
            69.999969, 17.46959000000004, 0, 0.333, 0.867, 1,
            70.05659899999999, 32.483490000000074, 0, 0.333, 0.867, 1,
            387.05461, 582.47441, 0, 0.333, 0.867, 1
        }
    },
    {
        {
            399.99999, 545.00001, 0, 0, 0.533, 0.667,
            279.80572, 332.84652, 0, 0, 0.533, 0.667,
            283.28458, 334.94335, 0, 0, 0.533, 0.667,
            286.823, 337.04604, 0, 0, 0.533, 0.667,
            341.97006999999996, 365.57160999999996, 0, 0, 0.533, 0.667,
            398.41644999999994, 379.19677, 0, 0, 0.533, 0.667,
            456.30409999999995, 366.24193, 0, 0, 0.533, 0.667,
            511.56239999999997, 337.54376, 0, 0, 0.533, 0.667,
            516.21412, 334.70753, 0, 0, 0.533, 0.667,
            520.7531799999999, 331.88219000000004, 0, 0, 0.533, 0.667
        },
        {
            399.41005, 332.84652, 0, 0, 0.533, 0.667,
            380.93985000000004, 329.08094, 0, 0, 0.533, 0.667,
            365.22407000000004, 318.7237, 0, 0, 0.533, 0.667,
            354.84185, 303.06385, 0, 0, 0.533, 0.667,
            351.06530000000004, 284.69203, 0, 0, 0.533, 0.667,
            354.84185, 266.30466, 0, 0, 0.533, 0.667,
            365.22407000000004, 250.62927000000002, 0, 0, 0.533, 0.667,
            380.93985000000004, 240.27202, 0, 0, 0.533, 0.667,
            399.41005, 236.50644, 0, 0, 0.533, 0.667,
            417.88025, 240.27202, 0, 0, 0.533, 0.667,
            433.59602, 250.62927000000002, 0, 0, 0.533, 0.667,
            443.96272, 266.30466, 0, 0, 0.533, 0.667,
            447.72373999999996, 284.69203, 0, 0, 0.533, 0.667,
            443.96272, 303.06385, 0, 0, 0.533, 0.667,
            433.59602, 318.7237, 0, 0, 0.533, 0.667,
            417.88025, 329.08094, 0, 0, 0.533, 0.667
        },
        {
            463.09346, 329.82909, 0, 0.667, 0.933, 1,
            473.79841, 308.43175, 0, 0.667, 0.933, 1,
            477.53168, 284.69203000000005, 0, 0.667, 0.933, 1,
            473.78272, 260.88570000000004, 0, 0.667, 0.933, 1,
            463.03136, 239.43055000000004, 0, 0.667, 0.933, 1,
            479.95043, 248.07354000000004, 0, 0.667, 0.933, 1,
            496.19268, 257.34849, 0, 0.667, 0.933, 1,
            519.28872, 272.00300000000004, 0, 0.667, 0.933, 1,
            537.52014, 284.62982000000005, 0, 0.667, 0.933, 1,
            519.29354, 297.26964000000004, 0, 0.667, 0.933, 1,
            496.19268, 311.94224, 0, 0.667, 0.933, 1,
            479.98079, 321.20174000000003, 0, 0.667, 0.933, 1
        },
        {
            335.19879, 329.08251, 0, 0.667, 0.933, 1,
            318.26334999999995, 320.48826, 0, 0.667, 0.933, 1,
            301.94431, 311.32009000000005, 0, 0.667, 0.933, 1,
            278.85826, 297.0322100000001, 0, 0.667, 0.933, 1,
            260.30636, 284.62982000000005, 0, 0.667, 0.933, 1,
            278.85830999999996, 272.2274100000001, 0, 0.667, 0.933, 1,
            301.94431, 257.93955000000005, 0, 0.667, 0.933, 1,
            318.30906999999996, 248.73796000000004, 0, 0.667, 0.933, 1,
            335.29193999999995, 240.11491000000007, 0, 0.667, 0.933, 1,
            324.88545999999997, 261.2835100000001, 0, 0.667, 0.933, 1,
            321.25737, 284.6920300000001, 0, 0.667, 0.933, 1,
            324.86325999999997, 307.9987500000001, 0, 0.667, 0.933, 1
        },
        {
            559.81399, 263.91219, 0, 0, 0.533, 0.667,
            539.20385, 249.42228, 0, 0, 0.533, 0.667,
            511.56239999999997, 231.74698999999998, 0, 0, 0.533, 0.667,
            456.30409999999995, 203.03328, 0, 0, 0.533, 0.667,
            398.41644999999994, 190.06286999999998, 0, 0, 0.533, 0.667,
            341.97007, 203.68802, 0, 0, 0.533, 0.667,
            286.823, 232.21359999999999, 0, 0, 0.533, 0.667,
            260.06127, 248.86752, 0, 0, 0.533, 0.667,
            239.53395, 262.76121, 0, 0, 0.533, 0.667,
            109, 39.999990000000025, 0, 0, 0.533, 0.667,
            399.87579, 42.14648999999997, 0, 0, 0.533, 0.667,
            400.1242, 42.14648999999997, 0, 0, 0.533, 0.667,
            691, 39.999990000000025, 0, 0, 0.533, 0.667
        }
    },
    {
        {
            399.4036, 312.84652, 0, 0, 0.267, 0.333,
            379.4115, 304.58744, 0, 0, 0.267, 0.333,
            371.13139, 284.68557000000004, 0, 0, 0.267, 0.333,
            379.4115, 264.76553, 0, 0, 0.267, 0.333,
            399.4036, 256.50644, 0, 0, 0.267, 0.333,
            419.3957, 264.76553, 0, 0, 0.267, 0.333,
            427.65765, 284.68557, 0, 0, 0.267, 0.333,
            419.3957, 304.58743999999996, 0, 0, 0.267, 0.333
        }
    }
};

If all has gone according to plan, you should now be ready to set up the tessellator.

Storing the Indexes

final List<Integer> allIndices = new ArrayList<>();

As usual you should start by creating a list in which to store the tessellation results. This time, however, you will be storing indices, rather than actual vertices, so you should make it a list of integers, rather than a list of floating point numbers.

GLUtessellator tessellator = gluNewTess();
GLUtessellatorCallback callback = new GLUtessellatorCallbackAdapter() {
    int type;
    List<Integer> indices = new ArrayList<>();
    @Override
    public void begin(int type) {
        this.type = type;
        indices.clear();
    }
    @Override
    public void end() {
        switch (type) {
        case GL_TRIANGLES:
            break;
        case GL_TRIANGLE_FAN:
            this.indices = fanToTriangles(indices);
            break;
        case GL_TRIANGLE_STRIP:
            this.indices = stripToTriangles(indices);
            break;
        }
        allIndices.addAll(indices);
    }
    @Override
    public void vertex(Object data) {
        indices.add((int) data);
    }
};
tessellator.gluTessCallback(GLU_TESS_BEGIN, callback);
tessellator.gluTessCallback(GLU_TESS_END, callback);
tessellator.gluTessCallback(GLU_TESS_VERTEX, callback);
tessellator.gluTessCallback(GLU_TESS_COMBINE, callback);

Next define the tessellator and a basic three-method callback object. The callback methods are similar to the ones defined in previous tutorials, except in that they work with indices.

@Override
public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) {
    int combined = -1;
    float highestWeight = -1;
    for (int i = 0; i < data.length; i++) {
        int index = (int) data[i];
        if (weight[i] > highestWeight) {
            highestWeight = weight[i];
            combined = index;
        }
    }
    outData[0] = combined;
}

Although the Eye of Providence example shape does not ever require the combine method, it can’t hurt to have one. It would be very difficult to actually combine the vertices in question, seeing as we don’t have access to them from the tessellator. Instead I have elected to pick the index of the vertex with the highest weight and return that. Feel free to experiment with more sophisticated techniques for combining the vertices.

public List<Integer> stripToTriangles(List<Integer> strip) {
    List<Integer> triangles = new ArrayList<>();
    for (int i = 0; i < strip.size() - 2; i++) {
        triangles.addAll(strip.subList(i, i + 3));
    }
    return triangles;
}
public List<Integer> fanToTriangles(List<Integer> fan) {
    int centralVertex = fan.get(0);
    List<Integer> triangles = new ArrayList<>();
    for (int i = 1; i < fan.size() - 1; i++) {
        triangles.add(centralVertex);
        triangles.addAll(fan.subList(i, i + 2));
    }
    return triangles;
}

The triangle strip and fan conversion functions I provided in the previous tutorial also require some tweaking to work with indices. The only absolutely necessary change is to divide all the indexes and incrementors used by 6.

List<Double> allVertices = new ArrayList<>();
for (double[][] polygon : eyeOfProvidence) {
    tessellator.gluTessBeginPolygon(null);
    for (double[] contour : polygon) {
        tessellator.gluTessBeginContour();
        for (int i = 0; i < contour.length - 5; i += 6) {
            double[] coordinates = {contour[i], contour[i + 1], contour[i + 2]};
            allVertices.addAll(Arrays.asList(
                contour[i], contour[i + 1], contour[i + 2],
                contour[i + 3], contour[i + 4], contour[i + 5]
            ));
            int index = (allVertices.size() - 6) / 6;
            tessellator.gluTessVertex(coordinates, 0, index);
        }
        tessellator.gluTessEndContour();
    }
    tessellator.gluTessEndPolygon();
}

In the tessellation loop you should now store every vertex encountered into a list, and then pass the index of the vertex to the tessellator as the data parameter. It is worth noting that OpenGL is going to expect the index of the vertex, NOT the index of the first coordinate of the vertex. For example, if allVertices contained the two vertices {399, 590, 0, 0.3, 0.8, 1, 412, 582, 0, 0.3, 0.8, 1}, then the index of the second vertex (the one starting with 412) would be 1, not 6. This is why you need to divide the index by 6 before passing it to the tessellator.

FloatBuffer vertices = BufferUtils.createFloatBuffer(allVertices.size());
for (double d : allVertices) vertices.put((float) d);
vertices.flip();
int vertexHandle = glGenBuffers();
glBindBuffer(GL_ARRAY_BUFFER, vertexHandle);
glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
glVertexPointer(3, GL_FLOAT, 6 << 2, 0);
glColorPointer(3, GL_FLOAT, 6 << 2, 3 << 2);

IntBuffer indices = BufferUtils.createIntBuffer(allIndices.size());
for (int i : allIndices) indices.put(i);
indices.flip();
int indexHandle = glGenBuffers();
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexHandle);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);

This time, in addition to loading the vertices into a buffer, you should also load the indices into a buffer of their own. Bind the buffer to the GL_ELEMENT_ARRAY_BUFFER instead of the GL_ARRAY_BUFFER target; this tells OpenGL to use read this buffer for indices when a call to glDrawElements() is made.

glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
while (!Display.isCloseRequested()) {
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glDrawElements(GL_TRIANGLES, allIndices.size(), GL_UNSIGNED_INT, 0);
    Display.update();
    Display.sync(60);
}
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

It is finally time to render the shape to the screen. Notice that inside the rendering loop we call glDrawElements() instead of glDrawArrays(). The parameters are relatively self-explanatory. GL_UNSIGNED_INT tells OpenGL that our indices are stored as ints, rather than as shorts or bytes. If you are drawing something with relatively few indices (fewer than 32,767), it makes sense to save memory by using a ShortBuffer instead of an IntBuffer and to use GL_UNSIGNED_SHORT instead. The last parameter is offset which is set to 0, as usual.

If you followed my instructions carefully, you should now be able to run your program and see the Eye of Providence staring into your soul. If you do see it, close it quickly before you need to be institutionalized, otherwise check your code against Appendix B.

Keyboard Input and Camera Manipulation

There comes a time in the life of every data visualization application that the user wants to control the view in some way or another. In this twice-foretold section I will discuss listening for keyboard input, and panning and zooming the 2D orthographic camera.

Listening For Keyboard Input

It’s worth noting that the code in this section is specific to LWJGL, and may or may not apply to other programming languages or toolkits.

LWJGL automatically takes care of polling user input when you call Display.update(). The only thing left for you to do is decide what to do with that input. First though, you should import the necessary classes, functions, and constants.

import static org.lwjgl.input.Keyboard.*;

Now you can start checking if a key has been pressed in the last frame from the rendering loop.

if (Keyboard.isKeyDown(KEY_Q)) System.out.println("Q has been pressed");

If you open your program and hold down the Q key you should see the phrase “Q has been pressed” printed several times to the console.

Manipulating the Camera

OpenGL provides some functions such as glTranslate() and glRotate() which allow you to easily manipulate the object pictured on screen. Unfortunately it always does this relative to the origin, and does not make it easy to scale the speed. As such, it is much easy to simply update the view with calls to glOrtho() on every frame. Before the rendering loop begins, define some variables to keep track of the state of the view.

final int WIDTH = 800, HEIGHT = 600;
double scale = 1, scaleFactor = 0.97, centerX = WIDTH / 2, centerY = HEIGHT / 2, speed = 10;

Then, inside the rendering loop, adjust the variables according to taste.

if (Keyboard.isKeyDown(KEY_Q)) {
    scale *= scaleFactor;
}
if (Keyboard.isKeyDown(KEY_A)) {
    scale /= scaleFactor;
}
if (Keyboard.isKeyDown(KEY_UP)) {
    centerY += scale*speed;
}
if (Keyboard.isKeyDown(KEY_DOWN)) {
    centerY -= scale*speed;
}
if (Keyboard.isKeyDown(KEY_LEFT)) {
    centerX -= scale*speed;
}
if (Keyboard.isKeyDown(KEY_RIGHT)) {
    centerX += scale*speed;
}

Finally, you can do some basic math and adjust the view.

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
glOrtho(centerX - WIDTH / 2 * scale, centerX + WIDTH / 2 * scale, centerY - HEIGHT / 2 * scale, centerY + HEIGHT / 2 * scale, 1, -1);

If all went according to plan you should be able to pan and zoom your display using the keys specified in the code.

Goodbye Stranger

This marks the end of the third (and most likely final) part of my tutorial on basic OpenGL in Java. I hope it was of assistance, and please contact me with questions or comments. Thank you for attention!

Appendix A: Screenshot of Indexed VBO Example

Reference Screenshot

Appendix B: Complete Source Code of Indexed VBO Example

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.util.glu.GLU.*;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.lwjgl.BufferUtils;
import org.lwjgl.LWJGLException;
import org.lwjgl.opengl.Display;
import org.lwjgl.opengl.DisplayMode;
import org.lwjgl.opengl.PixelFormat;
import org.lwjgl.util.glu.GLUtessellator;
import org.lwjgl.util.glu.GLUtessellatorCallback;
import org.lwjgl.util.glu.GLUtessellatorCallbackAdapter;

public class OpenGL2dDemo {

    public static void main(String[] argv) {
        new OpenGL2dDemo().start();
    }

    public void start() {
        try {
            Display.setDisplayMode(new DisplayMode(800, 600));
            Display.create(new PixelFormat(0, 8, 0, 0));
        } catch (LWJGLException e) {
            e.printStackTrace();
            System.exit(1);
        }

        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0, 800, 0, 600, 1, -1);
        glMatrixMode(GL_MODELVIEW);

        double[][][] eyeOfProvidence = {
            {
                {
                    399.31702, 590.00001, 0, 0.333, 0.867, 1,
                    412.94567, 582.47441, 0, 0.333, 0.867, 1,
                    729.94367, 32.483490000000074, 0, 0.333, 0.867, 1,
                    729.99997, 17.46959000000004, 0, 0.333, 0.867, 1,
                    716.9979099999999, 9.999990000000025, 0, 0.333, 0.867, 1,
                    400.12413999999995, 12.332290000000057, 0, 0.333, 0.867, 1,
                    399.87577999999996, 12.332290000000057, 0, 0.333, 0.867, 1,
                    83.00200799999999, 9.999990000000025, 0, 0.333, 0.867, 1,
                    69.999969, 17.46959000000004, 0, 0.333, 0.867, 1,
                    70.05659899999999, 32.483490000000074, 0, 0.333, 0.867, 1,
                    387.05461, 582.47441, 0, 0.333, 0.867, 1
                }
            },
            {
                {
                    399.99999, 545.00001, 0, 0, 0.533, 0.667,
                    279.80572, 332.84652, 0, 0, 0.533, 0.667,
                    283.28458, 334.94335, 0, 0, 0.533, 0.667,
                    286.823, 337.04604, 0, 0, 0.533, 0.667,
                    341.97006999999996, 365.57160999999996, 0, 0, 0.533, 0.667,
                    398.41644999999994, 379.19677, 0, 0, 0.533, 0.667,
                    456.30409999999995, 366.24193, 0, 0, 0.533, 0.667,
                    511.56239999999997, 337.54376, 0, 0, 0.533, 0.667,
                    516.21412, 334.70753, 0, 0, 0.533, 0.667,
                    520.7531799999999, 331.88219000000004, 0, 0, 0.533, 0.667
                },
                {
                    399.41005, 332.84652, 0, 0, 0.533, 0.667,
                    380.93985000000004, 329.08094, 0, 0, 0.533, 0.667,
                    365.22407000000004, 318.7237, 0, 0, 0.533, 0.667,
                    354.84185, 303.06385, 0, 0, 0.533, 0.667,
                    351.06530000000004, 284.69203, 0, 0, 0.533, 0.667,
                    354.84185, 266.30466, 0, 0, 0.533, 0.667,
                    365.22407000000004, 250.62927000000002, 0, 0, 0.533, 0.667,
                    380.93985000000004, 240.27202, 0, 0, 0.533, 0.667,
                    399.41005, 236.50644, 0, 0, 0.533, 0.667,
                    417.88025, 240.27202, 0, 0, 0.533, 0.667,
                    433.59602, 250.62927000000002, 0, 0, 0.533, 0.667,
                    443.96272, 266.30466, 0, 0, 0.533, 0.667,
                    447.72373999999996, 284.69203, 0, 0, 0.533, 0.667,
                    443.96272, 303.06385, 0, 0, 0.533, 0.667,
                    433.59602, 318.7237, 0, 0, 0.533, 0.667,
                    417.88025, 329.08094, 0, 0, 0.533, 0.667
                },
                {
                    463.09346, 329.82909, 0, 0.667, 0.933, 1,
                    473.79841, 308.43175, 0, 0.667, 0.933, 1,
                    477.53168, 284.69203000000005, 0, 0.667, 0.933, 1,
                    473.78272, 260.88570000000004, 0, 0.667, 0.933, 1,
                    463.03136, 239.43055000000004, 0, 0.667, 0.933, 1,
                    479.95043, 248.07354000000004, 0, 0.667, 0.933, 1,
                    496.19268, 257.34849, 0, 0.667, 0.933, 1,
                    519.28872, 272.00300000000004, 0, 0.667, 0.933, 1,
                    537.52014, 284.62982000000005, 0, 0.667, 0.933, 1,
                    519.29354, 297.26964000000004, 0, 0.667, 0.933, 1,
                    496.19268, 311.94224, 0, 0.667, 0.933, 1,
                    479.98079, 321.20174000000003, 0, 0.667, 0.933, 1
                },
                {
                    335.19879, 329.08251, 0, 0.667, 0.933, 1,
                    318.26334999999995, 320.48826, 0, 0.667, 0.933, 1,
                    301.94431, 311.32009000000005, 0, 0.667, 0.933, 1,
                    278.85826, 297.0322100000001, 0, 0.667, 0.933, 1,
                    260.30636, 284.62982000000005, 0, 0.667, 0.933, 1,
                    278.85830999999996, 272.2274100000001, 0, 0.667, 0.933, 1,
                    301.94431, 257.93955000000005, 0, 0.667, 0.933, 1,
                    318.30906999999996, 248.73796000000004, 0, 0.667, 0.933, 1,
                    335.29193999999995, 240.11491000000007, 0, 0.667, 0.933, 1,
                    324.88545999999997, 261.2835100000001, 0, 0.667, 0.933, 1,
                    321.25737, 284.6920300000001, 0, 0.667, 0.933, 1,
                    324.86325999999997, 307.9987500000001, 0, 0.667, 0.933, 1
                },
                {
                    559.81399, 263.91219, 0, 0, 0.533, 0.667,
                    539.20385, 249.42228, 0, 0, 0.533, 0.667,
                    511.56239999999997, 231.74698999999998, 0, 0, 0.533, 0.667,
                    456.30409999999995, 203.03328, 0, 0, 0.533, 0.667,
                    398.41644999999994, 190.06286999999998, 0, 0, 0.533, 0.667,
                    341.97007, 203.68802, 0, 0, 0.533, 0.667,
                    286.823, 232.21359999999999, 0, 0, 0.533, 0.667,
                    260.06127, 248.86752, 0, 0, 0.533, 0.667,
                    239.53395, 262.76121, 0, 0, 0.533, 0.667,
                    109, 39.999990000000025, 0, 0, 0.533, 0.667,
                    399.87579, 42.14648999999997, 0, 0, 0.533, 0.667,
                    400.1242, 42.14648999999997, 0, 0, 0.533, 0.667,
                    691, 39.999990000000025, 0, 0, 0.533, 0.667
                }
            },
            {
                {
                    399.4036, 312.84652, 0, 0, 0.267, 0.333,
                    379.4115, 304.58744, 0, 0, 0.267, 0.333,
                    371.13139, 284.68557000000004, 0, 0, 0.267, 0.333,
                    379.4115, 264.76553, 0, 0, 0.267, 0.333,
                    399.4036, 256.50644, 0, 0, 0.267, 0.333,
                    419.3957, 264.76553, 0, 0, 0.267, 0.333,
                    427.65765, 284.68557, 0, 0, 0.267, 0.333,
                    419.3957, 304.58743999999996, 0, 0, 0.267, 0.333
                }
            }
        };

        final List<Integer> allIndices = new ArrayList<>();

        GLUtessellator tessellator = gluNewTess();
        GLUtessellatorCallback callback = new GLUtessellatorCallbackAdapter() {
            int type;
            List<Integer> indices = new ArrayList<>();
            @Override
            public void begin(int type) {
                this.type = type;
                indices.clear();
            }
            @Override
            public void end() {
                switch (type) {
                case GL_TRIANGLES:
                    break;
                case GL_TRIANGLE_FAN:
                    this.indices = fanToTriangles(indices);
                    break;
                case GL_TRIANGLE_STRIP:
                    this.indices = stripToTriangles(indices);
                    break;
                }
                allIndices.addAll(indices);
            }
            @Override
            public void vertex(Object data) {
                indices.add((int) data);
            }
            @Override
            public void combine(double[] coords, Object[] data, float[] weight, Object[] outData) {
                int combined = -1;
                float highestWeight = -1;
                for (int i = 0; i < data.length; i++) {
                    int index = (int) data[i];
                    if (weight[i] > highestWeight) {
                        highestWeight = weight[i];
                        combined = index;
                    }
                }
                outData[0] = combined;
            }
            public List<Integer> stripToTriangles(List<Integer> strip) {
                List<Integer> triangles = new ArrayList<>();
                for (int i = 0; i < strip.size() - 2; i++) {
                    triangles.addAll(strip.subList(i, i + 3));
                }
                return triangles;
            }
            public List<Integer> fanToTriangles(List<Integer> fan) {
                int centralVertex = fan.get(0);
                List<Integer> triangles = new ArrayList<>();
                for (int i = 1; i < fan.size() - 1; i++) {
                    triangles.add(centralVertex);
                    triangles.addAll(fan.subList(i, i + 2));
                }
                return triangles;
            }
        };
        tessellator.gluTessCallback(GLU_TESS_BEGIN, callback);
        tessellator.gluTessCallback(GLU_TESS_END, callback);
        tessellator.gluTessCallback(GLU_TESS_VERTEX, callback);
        tessellator.gluTessCallback(GLU_TESS_COMBINE, callback);

        ArrayList<Double> allVertices = new ArrayList<>();
        for (double[][] polygon : eyeOfProvidence) {
            tessellator.gluTessBeginPolygon(null);
            for (double[] contour : polygon) {
                tessellator.gluTessBeginContour();
                for (int i = 0; i < contour.length - 5; i += 6) {
                    double[] coordinates = {contour[i], contour[i + 1], contour[i + 2]};
                    allVertices.addAll(Arrays.asList(
                        contour[i], contour[i + 1], contour[i + 2],
                        contour[i + 3], contour[i + 4], contour[i + 5]
                    ));
                    int index = (allVertices.size() - 6) / 6;
                    tessellator.gluTessVertex(coordinates, 0, index);
                }
                tessellator.gluTessEndContour();
            }
            tessellator.gluTessEndPolygon();
        }

        FloatBuffer vertices = BufferUtils.createFloatBuffer(allVertices.size());
        for (double d : allVertices) vertices.put((float) d);
        vertices.flip();
        int vertexHandle = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexHandle);
        glBufferData(GL_ARRAY_BUFFER, vertices, GL_STATIC_DRAW);
        glVertexPointer(3, GL_FLOAT, 6 << 2, 0);
        glColorPointer(3, GL_FLOAT, 6 << 2, 3 << 2);

        IntBuffer indices = BufferUtils.createIntBuffer(allIndices.size());
        for (int i : allIndices) indices.put(i);
        indices.flip();
        int indexHandle = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexHandle);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices, GL_STATIC_DRAW);

        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_COLOR_ARRAY);
        while (!Display.isCloseRequested()) {
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glDrawElements(GL_TRIANGLES, allIndices.size(), GL_UNSIGNED_INT, 0);
            Display.update();
            Display.sync(60);
        }
        glDisableClientState(GL_COLOR_ARRAY);
        glDisableClientState(GL_VERTEX_ARRAY);
    }

}