public class Framework {
    public static String COMMON_DATAPATH = "/integeruser/jgltut/data/";
    public static String CURRENT_TUTORIAL_DATAPATH = null;


    public static String findFileOrThrow(String fileName) {
        // search the file in '/jgltut/tut##/data/'
        InputStream fileStream = Framework.class.getResourceAsStream(CURRENT_TUTORIAL_DATAPATH + fileName);
        if (fileStream != null) return CURRENT_TUTORIAL_DATAPATH + fileName;

        // search the file in '/jgltut/data/'
        fileStream = Framework.class.getResourceAsStream(COMMON_DATAPATH + fileName);
        if (fileStream != null) return COMMON_DATAPATH + fileName;

        throw new RuntimeException("Could not find the file " + fileName);
    }

    ////////////////////////////////
    public static int loadShader(int shaderType, String shaderFilename) {
        String filePath = Framework.findFileOrThrow(shaderFilename);
        String shaderCode = loadShaderFile(filePath);
        return Shader.compileShader(shaderType, shaderCode);
    }

    private static String loadShaderFile(String shaderFilePath) {
        StringBuilder text = new StringBuilder();

        try {
            BufferedReader reader = new BufferedReader(new InputStreamReader(Framework.class.getResourceAsStream(shaderFilePath)));

            String line;
            while ((line = reader.readLine()) != null) {
                text.append(line).append("\n");
            }

            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return text.toString();
    }


    public static int createProgram(ArrayList<Integer> shaders) {
        try {
            int prog = Shader.linkProgram(shaders);
            return prog;
        } finally {
            for (Integer shader : shaders) {
                glDeleteShader(shader);
            }
        }
    }
} public class Tut1 extends Tutorial {
    public static void main(String[] args) {
        new Tut1().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();

        String vertexShader = "#version 330\n" +
                "layout(location = 0) in vec4 position;\n" +
                "void main()\n" +
                "{\n" +
                "   gl_Position = position;\n" +
                "}\n";
        shaderList.add(createShader(GL_VERTEX_SHADER, vertexShader));

        String fragmentShader = "#version 330\n" +
                "out vec4 outputColor;\n" +
                "void main()\n" +
                "{\n" +
                "   outputColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);\n" +
                "}\n";
        shaderList.add(createShader(GL_FRAGMENT_SHADER, fragmentShader));

        theProgram = createProgram(shaderList);

        for (Integer shader : shaderList) {
            glDeleteShader(shader);
        }
    }

    private int createShader(int shaderType, String shaderFile) {
        int shader = glCreateShader(shaderType);
        glShaderSource(shader, shaderFile);

        glCompileShader(shader);

        int status = glGetShaderi(shader, GL_COMPILE_STATUS);
        if (status == GL_FALSE) {
            int infoLogLength = glGetShaderi(shader, GL_INFO_LOG_LENGTH);
            String infoLog = glGetShaderInfoLog(shader, infoLogLength);

            String shaderTypeStr = null;
            switch (shaderType) {
                case GL_VERTEX_SHADER:
                    shaderTypeStr = "vertex";
                    break;
                case GL_GEOMETRY_SHADER:
                    shaderTypeStr = "geometry";
                    break;
                case GL_FRAGMENT_SHADER:
                    shaderTypeStr = "fragment";
                    break;
            }

            System.err.printf("Compile failure in %s shader:\n%s\n", shaderTypeStr, infoLog);
        }

        return shader;
    }

    private int createProgram(ArrayList<Integer> shaderList) {
        int program = glCreateProgram();

        for (Integer shader : shaderList) {
            glAttachShader(program, shader);
        }

        glLinkProgram(program);

        int status = glGetProgrami(program, GL_LINK_STATUS);
        if (status == GL_FALSE) {
            int infoLogLength = glGetProgrami(program, GL_INFO_LOG_LENGTH);
            String infoLog = glGetProgramInfoLog(program, infoLogLength);
            System.err.printf("Linker failure: %s\n", infoLog);
        }

        for (Integer shader : shaderList) {
            glDetachShader(program, shader);
        }

        return program;
    }

    ////////////////////////////////
    private final float[] vertexPositions = {
            0.75f, 0.75f, 0.0f, 1.0f,
            0.75f, -0.75f, 0.0f, 1.0f,
            -0.75f, -0.75f, 0.0f, 1.0f
    };

    private int positionBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexPositionsBuffer = BufferUtils.createFloatBuffer(vertexPositions.length);
        vertexPositionsBuffer.put(vertexPositions);
        vertexPositionsBuffer.flip();

        positionBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexPositionsBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class FragPosition extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut02/data/";
        new FragPosition().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "FragPosition.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "FragPosition.frag"));
        theProgram = Framework.createProgram(shaderList);
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.75f, 0.75f, 0.0f, 1.0f,
            0.75f, -0.75f, 0.0f, 1.0f,
            -0.75f, -0.75f, 0.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class VertexColor extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut02/data/";
        new VertexColor().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, 48);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "VertexColors.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "VertexColors.frag"));
        theProgram = Framework.createProgram(shaderList);
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.0f, 0.5f, 0.0f, 1.0f,
            0.5f, -0.366f, 0.0f, 1.0f,
            -0.5f, -0.366f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class CpuPositionOffset extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
        new CpuPositionOffset().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        xOffset = 0.0f;
        yOffset = 0.0f;
        computePositionOffsets();
        adjustVertexData();

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "Standard.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "Standard.frag"));
        theProgram = Framework.createProgram(shaderList);
    }

    ////////////////////////////////
    private final float[] vertexPositions = {
            0.25f, 0.25f, 0.0f, 1.0f,
            0.25f, -0.25f, 0.0f, 1.0f,
            -0.25f, -0.25f, 0.0f, 1.0f
    };

    private int positionBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexPositionsBuffer = BufferUtils.createFloatBuffer(vertexPositions.length);
        vertexPositionsBuffer.put(vertexPositions);
        vertexPositionsBuffer.flip();

        positionBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexPositionsBuffer, GL_STREAM_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private float xOffset, yOffset;


    private void computePositionOffsets() {
        final float loopDuration = 5.0f;
        final float scale = 3.14159f * 2.0f / loopDuration;
        final float currTimeThroughLoop = elapsedTime % loopDuration;
        xOffset = (float) (Math.cos(currTimeThroughLoop * scale) * 0.5f);
        yOffset = (float) (Math.sin(currTimeThroughLoop * scale) * 0.5f);
    }

    private void adjustVertexData() {
        float newData[] = Arrays.copyOf(vertexPositions, vertexPositions.length);
        for (int vertex = 0; vertex < vertexPositions.length; vertex += 4) {
            newData[vertex] += xOffset;
            newData[vertex + 1] += yOffset;
        }

        FloatBuffer newDataBuffer = BufferUtils.createFloatBuffer(newData.length);
        newDataBuffer.put(newData);
        newDataBuffer.flip();

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferSubData(GL_ARRAY_BUFFER, 0, newDataBuffer);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class FragChangeColor extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
        new FragChangeColor().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform1f(elapsedTimeUniform, elapsedTime);

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int elapsedTimeUniform;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "CalcOffset.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "CalcColor.frag"));
        theProgram = Framework.createProgram(shaderList);

        elapsedTimeUniform = glGetUniformLocation(theProgram, "time");

        glUseProgram(theProgram);
        int uniformLoopDuration = glGetUniformLocation(theProgram, "loopDuration");
        glUniform1f(uniformLoopDuration, 5.0f);
        int fragLoopDurUnf = glGetUniformLocation(theProgram, "fragLoopDuration");
        glUniform1f(fragLoopDurUnf, 10.0f);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final float[] vertexPositions = {
            0.25f, 0.25f, 0.0f, 1.0f,
            0.25f, -0.25f, 0.0f, 1.0f,
            -0.25f, -0.25f, 0.0f, 1.0f
    };

    private int positionBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexPositionsBuffer = BufferUtils.createFloatBuffer(vertexPositions.length);
        vertexPositionsBuffer.put(vertexPositions);
        vertexPositionsBuffer.flip();

        positionBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexPositionsBuffer, GL_STREAM_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class VertCalcOffset extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
        new VertCalcOffset().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform1f(elapsedTimeUniform, elapsedTime);

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int elapsedTimeUniform;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "CalcOffset.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "Standard.frag"));
        theProgram = Framework.createProgram(shaderList);

        elapsedTimeUniform = glGetUniformLocation(theProgram, "time");

        glUseProgram(theProgram);
        int loopDurationUnf = glGetUniformLocation(theProgram, "loopDuration");
        glUniform1f(loopDurationUnf, 5.0f);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final float[] vertexPositions = {
            0.25f, 0.25f, 0.0f, 1.0f,
            0.25f, -0.25f, 0.0f, 1.0f,
            -0.25f, -0.25f, 0.0f, 1.0f
    };

    private int positionBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexPositionsBuffer = BufferUtils.createFloatBuffer(vertexPositions.length);
        vertexPositionsBuffer.put(vertexPositions);
        vertexPositionsBuffer.flip();

        positionBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexPositionsBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class VertPositionOffset extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
        new VertPositionOffset().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);
    }

    @Override
    protected void display() {
        xOffset = 0.0f;
        yOffset = 0.0f;
        computePositionOffsets();

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform2f(offsetLocation, xOffset, yOffset);

        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);

        glDrawArrays(GL_TRIANGLES, 0, 3);

        glDisableVertexAttribArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int offsetLocation;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PositionOffset.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "Standard.frag"));
        theProgram = Framework.createProgram(shaderList);

        offsetLocation = glGetUniformLocation(theProgram, "offset");
    }

    ////////////////////////////////
    private final float[] vertexPositions = {
            0.25f, 0.25f, 0.0f, 1.0f,
            0.25f, -0.25f, 0.0f, 1.0f,
            -0.25f, -0.25f, 0.0f, 1.0f
    };

    private int positionBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexPositionsBuffer = BufferUtils.createFloatBuffer(vertexPositions.length);
        vertexPositionsBuffer.put(vertexPositions);
        vertexPositionsBuffer.flip();

        positionBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexPositionsBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private float xOffset, yOffset;


    private void computePositionOffsets() {
        final float loopDuration = 5.0f;
        final float scale = 3.14159f * 2.0f / loopDuration;
        final float currTimeThroughLoop = elapsedTime % loopDuration;
        xOffset = (float) (Math.cos(currTimeThroughLoop * scale) * 0.5f);
        yOffset = (float) (Math.sin(currTimeThroughLoop * scale) * 0.5f);
    }
} public class AspectRatio extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
        new AspectRatio().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform2f(offsetUniform, 1.5f, 0.5f);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);
        int colorData = (Float.BYTES * vertexData.length) / 2;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorData);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        perspectiveMatrix[0] = frustumScale / (w / (float) h);
        perspectiveMatrix[5] = frustumScale;

        FloatBuffer perspectiveMatrixBuffer = BufferUtils.createFloatBuffer(perspectiveMatrix.length);
        perspectiveMatrixBuffer.put(perspectiveMatrix);
        perspectiveMatrixBuffer.flip();

        glUseProgram(theProgram);
        glUniformMatrix4fv(perspectiveMatrixUnif, false, perspectiveMatrixBuffer);
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int offsetUniform;
    private int perspectiveMatrixUnif;

    private float perspectiveMatrix[];
    private final float frustumScale = 1.0f;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "MatrixPerspective.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "StandardColors.frag"));
        theProgram = Framework.createProgram(shaderList);

        offsetUniform = glGetUniformLocation(theProgram, "offset");
        perspectiveMatrixUnif = glGetUniformLocation(theProgram, "perspectiveMatrix");

        float zNear = 0.5f;
        float zFar = 3.0f;
        perspectiveMatrix = new float[16];
        perspectiveMatrix[0] = frustumScale;
        perspectiveMatrix[5] = frustumScale;
        perspectiveMatrix[10] = (zFar + zNear) / (zNear - zFar);
        perspectiveMatrix[14] = (2 * zFar * zNear) / (zNear - zFar);
        perspectiveMatrix[11] = -1.0f;

        FloatBuffer perspectiveMatrixBuffer = BufferUtils.createFloatBuffer(perspectiveMatrix.length);
        perspectiveMatrixBuffer.put(perspectiveMatrix);
        perspectiveMatrixBuffer.flip();

        glUseProgram(theProgram);
        glUniformMatrix4fv(perspectiveMatrixUnif, false, perspectiveMatrixBuffer);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,


            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class MatrixPerspective extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
        new MatrixPerspective().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform2f(offsetUniform, 0.5f, 0.5f);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);
        int colorData = (Float.BYTES * vertexData.length) / 2;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorData);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int offsetUniform;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "MatrixPerspective.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "StandardColors.frag"));
        theProgram = Framework.createProgram(shaderList);

        offsetUniform = glGetUniformLocation(theProgram, "offset");

        float frustumScale = 1.0f;
        float zNear = 0.5f;
        float zFar = 3.0f;
        float theMatrix[] = new float[16];
        theMatrix[0] = frustumScale;
        theMatrix[5] = frustumScale;
        theMatrix[10] = (zFar + zNear) / (zNear - zFar);
        theMatrix[14] = (2 * zFar * zNear) / (zNear - zFar);
        theMatrix[11] = -1.0f;

        FloatBuffer theMatrixBuffer = BufferUtils.createFloatBuffer(theMatrix.length);
        theMatrixBuffer.put(theMatrix);
        theMatrixBuffer.flip();

        glUseProgram(theProgram);
        int perspectiveMatrixUnif = glGetUniformLocation(theProgram, "perspectiveMatrix");
        glUniformMatrix4fv(perspectiveMatrixUnif, false, theMatrixBuffer);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,


            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class OrthoCube extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
        new OrthoCube().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform2f(offsetUniform, 0.5f, 0.25f);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);
        int colorData = (Float.BYTES * vertexData.length) / 2;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorData);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int offsetUniform;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "OrthoWithOffset.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "StandardColors.frag"));
        theProgram = Framework.createProgram(shaderList);

        offsetUniform = glGetUniformLocation(theProgram, "offset");
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.25f, 0.25f, 0.75f, 1.0f,
            0.25f, -0.25f, 0.75f, 1.0f,
            -0.25f, 0.25f, 0.75f, 1.0f,

            0.25f, -0.25f, 0.75f, 1.0f,
            -0.25f, -0.25f, 0.75f, 1.0f,
            -0.25f, 0.25f, 0.75f, 1.0f,

            0.25f, 0.25f, -0.75f, 1.0f,
            -0.25f, 0.25f, -0.75f, 1.0f,
            0.25f, -0.25f, -0.75f, 1.0f,

            0.25f, -0.25f, -0.75f, 1.0f,
            -0.25f, 0.25f, -0.75f, 1.0f,
            -0.25f, -0.25f, -0.75f, 1.0f,

            -0.25f, 0.25f, 0.75f, 1.0f,
            -0.25f, -0.25f, 0.75f, 1.0f,
            -0.25f, -0.25f, -0.75f, 1.0f,

            -0.25f, 0.25f, 0.75f, 1.0f,
            -0.25f, -0.25f, -0.75f, 1.0f,
            -0.25f, 0.25f, -0.75f, 1.0f,

            0.25f, 0.25f, 0.75f, 1.0f,
            0.25f, -0.25f, -0.75f, 1.0f,
            0.25f, -0.25f, 0.75f, 1.0f,

            0.25f, 0.25f, 0.75f, 1.0f,
            0.25f, 0.25f, -0.75f, 1.0f,
            0.25f, -0.25f, -0.75f, 1.0f,

            0.25f, 0.25f, -0.75f, 1.0f,
            0.25f, 0.25f, 0.75f, 1.0f,
            -0.25f, 0.25f, 0.75f, 1.0f,

            0.25f, 0.25f, -0.75f, 1.0f,
            -0.25f, 0.25f, 0.75f, 1.0f,
            -0.25f, 0.25f, -0.75f, 1.0f,

            0.25f, -0.25f, -0.75f, 1.0f,
            -0.25f, -0.25f, 0.75f, 1.0f,
            0.25f, -0.25f, 0.75f, 1.0f,

            0.25f, -0.25f, -0.75f, 1.0f,
            -0.25f, -0.25f, -0.75f, 1.0f,
            -0.25f, -0.25f, 0.75f, 1.0f,


            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class ShaderPerspective extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
        new ShaderPerspective().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        int vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glUseProgram(theProgram);

        glUniform2f(offsetUniform, 0.5f, 0.5f);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 4, GL_FLOAT, false, 0, 0);
        int colorData = (Float.BYTES * vertexData.length) / 2;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorData);

        glDrawArrays(GL_TRIANGLES, 0, 36);

        glDisableVertexAttribArray(0);
        glDisableVertexAttribArray(1);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int offsetUniform;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "ManualPerspective.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "StandardColors.frag"));
        theProgram = Framework.createProgram(shaderList);

        offsetUniform = glGetUniformLocation(theProgram, "offset");

        glUseProgram(theProgram);
        int frustumScaleUnif = glGetUniformLocation(theProgram, "frustumScale");
        glUniform1f(frustumScaleUnif, 1.0f);
        int zNearUnif = glGetUniformLocation(theProgram, "zNear");
        glUniform1f(zNearUnif, 1.0f);
        int zFarUnif = glGetUniformLocation(theProgram, "zFar");
        glUniform1f(zFarUnif, 3.0f);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final float[] vertexData = {
            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,

            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -1.25f, 1.0f,
            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, -0.25f, -2.75f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,

            0.25f, 0.25f, -2.75f, 1.0f,
            -0.25f, 0.25f, -1.25f, 1.0f,
            -0.25f, 0.25f, -2.75f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,
            0.25f, -0.25f, -1.25f, 1.0f,

            0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -2.75f, 1.0f,
            -0.25f, -0.25f, -1.25f, 1.0f,


            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,
            0.0f, 0.0f, 1.0f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,
            0.8f, 0.8f, 0.8f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,
            0.0f, 1.0f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,
            0.5f, 0.5f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,
            1.0f, 0.0f, 0.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,

            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f,
            0.0f, 1.0f, 1.0f, 1.0f
    };

    private int vertexBufferObject;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }
} public class Hierarchy extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut06/data/";
        new Hierarchy().start(700, 700);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVAO();

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        armature.writePose();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        armature.draw();
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale / (w / (float) h));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        if (isKeyPressed(GLFW_KEY_A)) {
            armature.adjBase(false);
        } else if (isKeyPressed(GLFW_KEY_D)) {
            armature.adjBase(true);
        }

        if (isKeyPressed(GLFW_KEY_W)) {
            armature.adjUpperArm(false);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            armature.adjUpperArm(true);
        }

        if (isKeyPressed(GLFW_KEY_R)) {
            armature.adjLowerArm(false);
        } else if (isKeyPressed(GLFW_KEY_F)) {
            armature.adjLowerArm(true);
        }

        if (isKeyPressed(GLFW_KEY_T)) {
            armature.adjWristPitch(false);
        } else if (isKeyPressed(GLFW_KEY_G)) {
            armature.adjWristPitch(true);
        }

        if (isKeyPressed(GLFW_KEY_Z)) {
            armature.adjWristRoll(false);
        } else if (isKeyPressed(GLFW_KEY_C)) {
            armature.adjWristRoll(true);
        }

        if (isKeyPressed(GLFW_KEY_Q)) {
            armature.adjFingerOpen(true);
        } else if (isKeyPressed(GLFW_KEY_E)) {
            armature.adjFingerOpen(false);
        }
    }

    ////////////////////////////////
    private int theProgram;
    private int positionAttrib;
    private int colorAttrib;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(45.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"));
        theProgram = Framework.createProgram(shaderList);

        positionAttrib = glGetAttribLocation(theProgram, "position");
        colorAttrib = glGetAttribLocation(theProgram, "color");

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");

        float zNear = 1.0f;
        float zFar = 100.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return 1.0f / (float) (Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private final float vertexData[] = {
            // Front
            +1.0f, +1.0f, +1.0f,
            +1.0f, -1.0f, +1.0f,
            -1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, +1.0f,

            // Top
            +1.0f, +1.0f, +1.0f,
            -1.0f, +1.0f, +1.0f,
            -1.0f, +1.0f, -1.0f,
            +1.0f, +1.0f, -1.0f,

            // Left
            +1.0f, +1.0f, +1.0f,
            +1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, -1.0f,
            +1.0f, -1.0f, +1.0f,

            // Back
            +1.0f, +1.0f, -1.0f,
            -1.0f, +1.0f, -1.0f,
            -1.0f, -1.0f, -1.0f,
            +1.0f, -1.0f, -1.0f,

            // Bottom
            +1.0f, -1.0f, +1.0f,
            +1.0f, -1.0f, -1.0f,
            -1.0f, -1.0f, -1.0f,
            -1.0f, -1.0f, +1.0f,

            // Right
            -1.0f, +1.0f, +1.0f,
            -1.0f, -1.0f, +1.0f,
            -1.0f, -1.0f, -1.0f,
            -1.0f, +1.0f, -1.0f,


            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR

            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR

            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR

            1.0f, 1.0f, 0.0f, 1.0f,  // YELLOW_COLOR
            1.0f, 1.0f, 0.0f, 1.0f,  // YELLOW_COLOR
            1.0f, 1.0f, 0.0f, 1.0f,  // YELLOW_COLOR
            1.0f, 1.0f, 0.0f, 1.0f,  // YELLOW_COLOR

            0.0f, 1.0f, 1.0f, 1.0f,  // CYAN_COLOR
            0.0f, 1.0f, 1.0f, 1.0f,  // CYAN_COLOR
            0.0f, 1.0f, 1.0f, 1.0f,  // CYAN_COLOR
            0.0f, 1.0f, 1.0f, 1.0f,  // CYAN_COLOR

            1.0f, 0.0f, 1.0f, 1.0f,  // MAGENTA_COLOR
            1.0f, 0.0f, 1.0f, 1.0f,  // MAGENTA_COLOR
            1.0f, 0.0f, 1.0f, 1.0f,  // MAGENTA_COLOR
            1.0f, 0.0f, 1.0f, 1.0f   // MAGENTA_COLOR
    };

    private final short indexData[] = {
            0, 1, 2,
            2, 3, 0,

            4, 5, 6,
            6, 7, 4,

            8, 9, 10,
            10, 11, 8,

            12, 13, 14,
            14, 15, 12,

            16, 17, 18,
            18, 19, 16,

            20, 21, 22,
            22, 23, 20
    };

    private int vao;


    private void initializeVAO() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        int vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        ShortBuffer indexDataBuffer = BufferUtils.createShortBuffer(indexData.length);
        indexDataBuffer.put(indexData);
        indexDataBuffer.flip();

        int indexBufferObject = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

        vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(positionAttrib);
        glEnableVertexAttribArray(colorAttrib);
        glVertexAttribPointer(positionAttrib, 3, GL_FLOAT, false, 0, 0);
        final int numberOfVertices = 24;
        int colorDataOffset = Float.BYTES * 3 * numberOfVertices;
        glVertexAttribPointer(colorAttrib, 4, GL_FLOAT, false, 0, colorDataOffset);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);

        glBindVertexArray(0);
    }

    ////////////////////////////////
    private final float STANDARD_ANGLE_INCREMENT = 11.25f;
    private final float SMALL_ANGLE_INCREMENT = 9.0f;

    private Armature armature = new Armature();

    private class Armature {
        Vector3f posBase;
        float angBase;

        Vector3f posBaseLeft, posBaseRight;
        float scaleBaseZ;

        float angUpperArm;
        float sizeUpperArm;

        Vector3f posLowerArm;
        float angLowerArm;
        float lenLowerArm;
        float widthLowerArm;

        Vector3f posWrist;
        float angWristRoll;
        float angWristPitch;
        float lenWrist;
        float widthWrist;

        Vector3f posLeftFinger, posRightFinger;
        float angFingerOpen;
        float lenFinger;
        float widthFinger;
        float angLowerFinger;


        Armature() {
            posBase = new Vector3f(3.0f, -5.0f, -40.0f);
            angBase = -45.0f;

            posBaseLeft = new Vector3f(2.0f, 0.0f, 0.0f);
            posBaseRight = new Vector3f(-2.0f, 0.0f, 0.0f);
            scaleBaseZ = 3.0f;

            angUpperArm = -70.75f;
            sizeUpperArm = 9.0f;

            posLowerArm = new Vector3f(0.0f, 0.0f, 8.0f);
            angLowerArm = 60.25f;
            lenLowerArm = 5.0f;
            widthLowerArm = 1.5f;

            posWrist = new Vector3f(0.0f, 0.0f, 5.0f);
            angWristRoll = 0.0f;
            angWristPitch = 67.5f;
            lenWrist = 2.0f;
            widthWrist = 2.0f;

            posLeftFinger = new Vector3f(1.0f, 0.0f, 1.0f);
            posRightFinger = new Vector3f(-1.0f, 0.0f, 1.0f);
            angFingerOpen = 45.0f;
            lenFinger = 2.0f;
            widthFinger = 0.5f;
            angLowerFinger = 45.0f;
        }


        void draw() {
            MatrixStackf modelToCameraStack = new MatrixStackf(10);

            glUseProgram(theProgram);
            glBindVertexArray(vao);

            modelToCameraStack.translate(posBase);
            modelToCameraStack.rotateY((float) Math.toRadians(angBase));

            // Draw left base.
            {
                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(posBaseLeft);
                modelToCameraStack.scale(new Vector3f(1.0f, 1.0f, scaleBaseZ));
                glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
                glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
                modelToCameraStack.popMatrix();
            }

            // Draw right base.
            {
                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(posBaseRight);
                modelToCameraStack.scale(new Vector3f(1.0f, 1.0f, scaleBaseZ));
                glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
                glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
                modelToCameraStack.popMatrix();
            }

            // Draw main arm.
            drawUpperArm(modelToCameraStack);

            glBindVertexArray(0);
            glUseProgram(0);
        }

        void drawUpperArm(MatrixStackf modelToCameraStack) {
            modelToCameraStack.pushMatrix();
            modelToCameraStack.rotateX((float) Math.toRadians(angUpperArm));

            {
                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, (sizeUpperArm / 2.0f) - 1.0f));
                modelToCameraStack.scale(new Vector3f(1.0f, 1.0f, sizeUpperArm / 2.0f));
                glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
                glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
                modelToCameraStack.popMatrix();
            }

            drawLowerArm(modelToCameraStack);

            modelToCameraStack.popMatrix();
        }

        void drawLowerArm(MatrixStackf modelToCameraStack) {
            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(posLowerArm);
            modelToCameraStack.rotateX((float) Math.toRadians(angLowerArm));

            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenLowerArm / 2.0f));
            modelToCameraStack.scale(new Vector3f(widthLowerArm / 2.0f, widthLowerArm / 2.0f, lenLowerArm / 2.0f));
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
            modelToCameraStack.popMatrix();

            drawWrist(modelToCameraStack);

            modelToCameraStack.popMatrix();
        }

        void drawWrist(MatrixStackf modelToCameraStack) {
            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(posWrist);
            modelToCameraStack.rotateZ((float) Math.toRadians(angWristRoll));
            modelToCameraStack.rotateX((float) Math.toRadians(angWristPitch));

            modelToCameraStack.pushMatrix();
            modelToCameraStack.scale(new Vector3f(widthWrist / 2.0f, widthWrist / 2.0f, lenWrist / 2.0f));
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
            modelToCameraStack.popMatrix();

            drawFingers(modelToCameraStack);

            modelToCameraStack.popMatrix();
        }

        void drawFingers(MatrixStackf modelToCameraStack) {
            // Draw left finger
            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(posLeftFinger);
            modelToCameraStack.rotateY((float) Math.toRadians(angFingerOpen));

            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger / 2.0f));
            modelToCameraStack.scale(new Vector3f(widthFinger / 2.0f, widthFinger / 2.0f, lenFinger / 2.0f));
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
            modelToCameraStack.popMatrix();

            {
                // Draw left lower finger
                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger));
                modelToCameraStack.rotateY((float) Math.toRadians(-angLowerFinger));

                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger / 2.0f));
                modelToCameraStack.scale(new Vector3f(widthFinger / 2.0f, widthFinger / 2.0f, lenFinger / 2.0f));
                glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
                glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
                modelToCameraStack.popMatrix();

                modelToCameraStack.popMatrix();
            }

            modelToCameraStack.popMatrix();

            // Draw right finger
            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(posRightFinger);
            modelToCameraStack.rotateY((float) Math.toRadians(-angFingerOpen));

            modelToCameraStack.pushMatrix();
            modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger / 2.0f));
            modelToCameraStack.scale(new Vector3f(widthFinger / 2.0f, widthFinger / 2.0f, lenFinger / 2.0f));
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
            modelToCameraStack.popMatrix();

            {
                // Draw right lower finger
                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger));
                modelToCameraStack.rotateY((float) Math.toRadians(angLowerFinger));

                modelToCameraStack.pushMatrix();
                modelToCameraStack.translate(new Vector3f(0.0f, 0.0f, lenFinger / 2.0f));
                modelToCameraStack.scale(new Vector3f(widthFinger / 2.0f, widthFinger / 2.0f, lenFinger / 2.0f));
                glUniformMatrix4fv(modelToCameraMatrixUnif, false, modelToCameraStack.get(mat4Buffer));
                glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
                modelToCameraStack.popMatrix();

                modelToCameraStack.popMatrix();
            }

            modelToCameraStack.popMatrix();
        }


        void adjBase(boolean increment) {
            final float scale = 5;
            angBase += increment ? STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale : -STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale;
            angBase = angBase % 360.0f;
        }

        void adjUpperArm(boolean increment) {
            final float scale = 5;
            angUpperArm += increment ? STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale : -STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale;
            angUpperArm = clamp(angUpperArm, -90.0f, 0.0f);
        }

        void adjLowerArm(boolean increment) {
            final float scale = 5;
            angLowerArm += increment ? STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale : -STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale;
            angLowerArm = clamp(angLowerArm, 0.0f, 146.25f);
        }

        void adjWristPitch(boolean increment) {
            final float scale = 5;
            angWristPitch += increment ? STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale : -STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale;
            angWristPitch = clamp(angWristPitch, 0.0f, 90.0f);
        }

        void adjWristRoll(boolean increment) {
            final float scale = 5;
            angWristRoll += increment ? STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale : -STANDARD_ANGLE_INCREMENT * lastFrameDuration * scale;
            angWristRoll = angWristRoll % 360.0f;
        }

        void adjFingerOpen(boolean increment) {
            final float scale = 5;
            angFingerOpen += increment ? SMALL_ANGLE_INCREMENT * lastFrameDuration * scale : -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
            angFingerOpen = clamp(angFingerOpen, 9.0f, 90.0f);
        }


        void writePose() {
            System.out.printf("angBase:\t%f\n", angBase);
            System.out.printf("angUpperArm:\t%f\n", angUpperArm);
            System.out.printf("angLowerArm:\t%f\n", angLowerArm);
            System.out.printf("angWristPitch:\t%f\n", angWristPitch);
            System.out.printf("angWristRoll:\t%f\n", angWristRoll);
            System.out.printf("angFingerOpen:\t%f\n", angFingerOpen);
            System.out.printf("\n");
        }
    }


    private float clamp(float value, float minValue, float maxValue) {
        if (value < minValue) return minValue;
        if (value > maxValue) return maxValue;
        return value;
    }
} public class Rotation extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut06/data/";
        new Rotation().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
        final int numberOfVertices = 8;
        int colorDataOffset = Float.BYTES * 3 * numberOfVertices;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorDataOffset);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);

        glBindVertexArray(0);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindVertexArray(vao);

        for (Instance currInst : instanceList) {
            final Matrix4f transformMatrix = currInst.constructMatrix(elapsedTime);
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, transformMatrix.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
        }

        glBindVertexArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale / (w / (float) h));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(45.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");

        float zNear = 1.0f;
        float zFar = 61.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private final float vertexData[] = {
            +1.0f, +1.0f, +1.0f,
            -1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, -1.0f,

            -1.0f, -1.0f, -1.0f,
            +1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, +1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f,  // BROWN_COLOR

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f   // BROWN_COLOR
    };

    private final short indexData[] = {
            0, 1, 2,
            1, 0, 3,
            2, 3, 0,
            3, 2, 1,

            5, 4, 6,
            4, 5, 7,
            7, 6, 4,
            6, 7, 5
    };

    private int vertexBufferObject;
    private int indexBufferObject;

    private int vao;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        ShortBuffer indexDataBuffer = BufferUtils.createShortBuffer(indexData.length);
        indexDataBuffer.put(indexData);
        indexDataBuffer.flip();

        indexBufferObject = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private Instance instanceList[] = {
            new NullRotation(new Vector3f(0.0f, 0.0f, -25.0f)),
            new RotateX(new Vector3f(-5.0f, -5.0f, -25.0f)),
            new RotateY(new Vector3f(-5.0f, 5.0f, -25.0f)),
            new RotateZ(new Vector3f(5.0f, 5.0f, -25.0f)),
            new RotateAxis(new Vector3f(5.0f, -5.0f, -25.0f))
    };

    private abstract class Instance {
        private Vector3f offset;

        Instance(Vector3f offset) {
            this.offset = offset;
        }

        abstract Matrix3f calcRotation(float elapsedTime);

        Matrix4f constructMatrix(float elapsedTime) {
            final Matrix3f rotMatrix = calcRotation(elapsedTime);

            Matrix4f theMat = new Matrix4f(rotMatrix);
            theMat.setTranslation(offset);
            return theMat;
        }
    }

    private class NullRotation extends Instance {
        NullRotation(Vector3f offset) {
            super(offset);
        }

        @Override
        Matrix3f calcRotation(float elapsedTime) {
            return new Matrix3f();
        }
    }

    private class RotateX extends Instance {
        RotateX(Vector3f offset) {
            super(offset);
        }

        @Override
        Matrix3f calcRotation(float elapsedTime) {
            float angRad = computeAngleRad(elapsedTime, 3.0f);
            float cos = (float) Math.cos(angRad);
            float sin = (float) Math.sin(angRad);

            Matrix3f theMat = new Matrix3f();
            theMat.m11(cos);
            theMat.m21(-sin);
            theMat.m12(sin);
            theMat.m22(cos);
            return theMat;
        }
    }

    private class RotateY extends Instance {
        RotateY(Vector3f offset) {
            super(offset);
        }

        @Override
        Matrix3f calcRotation(float elapsedTime) {
            float angRad = computeAngleRad(elapsedTime, 2.0f);
            float cos = (float) Math.cos(angRad);
            float sin = (float) Math.sin(angRad);

            Matrix3f theMat = new Matrix3f();
            theMat.m00(cos);
            theMat.m20(sin);
            theMat.m02(-sin);
            theMat.m22(cos);
            return theMat;
        }
    }

    private class RotateZ extends Instance {
        RotateZ(Vector3f offset) {
            super(offset);
        }

        @Override
        Matrix3f calcRotation(float elapsedTime) {
            float angRad = computeAngleRad(elapsedTime, 2.0f);
            float cos = (float) Math.cos(angRad);
            float sin = (float) Math.sin(angRad);

            Matrix3f theMat = new Matrix3f();
            theMat.m00(cos);
            theMat.m10(-sin);
            theMat.m01(sin);
            theMat.m11(cos);
            return theMat;
        }
    }

    private class RotateAxis extends Instance {
        RotateAxis(Vector3f offset) {
            super(offset);
        }

        @Override
        Matrix3f calcRotation(float elapsedTime) {
            float angRad = computeAngleRad(elapsedTime, 2.0f);
            float cos = (float) Math.cos(angRad);
            float invCos = 1.0f - cos;
            float sin = (float) Math.sin(angRad);

            Vector3f axis = new Vector3f(1.0f, 1.0f, 1.0f);
            axis.normalize();

            Matrix3f theMat = new Matrix3f();
            theMat.m00((axis.x * axis.x) + ((1 - axis.x * axis.x) * cos));
            theMat.m10(axis.x * axis.y * (invCos) - (axis.z * sin));
            theMat.m20(axis.x * axis.z * (invCos) + (axis.y * sin));
            theMat.m01(axis.x * axis.y * (invCos) + (axis.z * sin));
            theMat.m11((axis.y * axis.y) + ((1 - axis.y * axis.y) * cos));
            theMat.m21(axis.y * axis.z * (invCos) - (axis.x * sin));
            theMat.m02(axis.x * axis.z * (invCos) - (axis.y * sin));
            theMat.m12(axis.y * axis.z * (invCos) + (axis.x * sin));
            theMat.m22((axis.z * axis.z) + ((1 - axis.z * axis.z) * cos));
            return theMat;
        }
    }


    private float computeAngleRad(float elapsedTime, float loopDuration) {
        final float scale = 3.14159f * 2.0f / loopDuration;
        float currTimeThroughLoop = elapsedTime % loopDuration;
        return currTimeThroughLoop * scale;
    }
} public class Scale extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut06/data/";
        new Scale().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
        final int numberOfVertices = 8;
        int colorDataOffset = Float.BYTES * 3 * numberOfVertices;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorDataOffset);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);

        glBindVertexArray(0);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindVertexArray(vao);

        for (Instance currInst : instanceList) {
            final Matrix4f transformMatrix = currInst.constructMatrix(elapsedTime);
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, transformMatrix.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
        }

        glBindVertexArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale * (h / (float) w));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(45.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");

        float zNear = 1.0f;
        float zFar = 61.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private final float vertexData[] = {
            +1.0f, +1.0f, +1.0f,
            -1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, -1.0f,

            -1.0f, -1.0f, -1.0f,
            +1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, +1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f,  // BROWN_COLOR

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f   // BROWN_COLOR
    };

    private final short indexData[] = {
            0, 1, 2,
            1, 0, 3,
            2, 3, 0,
            3, 2, 1,

            5, 4, 6,
            4, 5, 7,
            7, 6, 4,
            6, 7, 5
    };

    private int vertexBufferObject;
    private int indexBufferObject;

    private int vao;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        ShortBuffer indexDataBuffer = BufferUtils.createShortBuffer(indexData.length);
        indexDataBuffer.put(indexData);
        indexDataBuffer.flip();

        indexBufferObject = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private Instance instanceList[] = {
            new NullScale(new Vector3f(0.0f, 0.0f, -45.0f)),
            new StaticUniformScale(new Vector3f(-10.0f, -10.0f, -45.0f)),
            new StaticNonUniformScale(new Vector3f(-10.0f, 10.0f, -45.0f)),
            new DynamicUniformScale(new Vector3f(10.0f, 10.0f, -45.0f)),
            new DynamicNonUniformScale(new Vector3f(10.0f, -10.0f, -45.0f))
    };

    private abstract class Instance {
        private Vector3f offset;

        Instance(Vector3f offset) {
            this.offset = offset;
        }

        abstract Vector3f calcScale(float elapsedTime);

        Matrix4f constructMatrix(float elapsedTime) {
            Vector3f theScale = calcScale(elapsedTime);

            Matrix4f theMat = new Matrix4f();
            theMat.m00(theScale.x);
            theMat.m11(theScale.y);
            theMat.m22(theScale.z);
            theMat.setTranslation(offset);
            return theMat;
        }
    }

    private class NullScale extends Instance {
        NullScale(Vector3f offset) {
            super(offset);
        }

        @Override
        Vector3f calcScale(float elapsedTime) {
            return new Vector3f(1.0f, 1.0f, 1.0f);
        }
    }

    private class StaticUniformScale extends Instance {
        StaticUniformScale(Vector3f offset) {
            super(offset);
        }

        @Override
        Vector3f calcScale(float elapsedTime) {
            return new Vector3f(4.0f, 4.0f, 4.0f);
        }
    }

    private class StaticNonUniformScale extends Instance {
        StaticNonUniformScale(Vector3f offset) {
            super(offset);
        }

        @Override
        Vector3f calcScale(float elapsedTime) {
            return new Vector3f(0.5f, 1.0f, 10.0f);
        }
    }

    private class DynamicUniformScale extends Instance {
        DynamicUniformScale(Vector3f offset) {
            super(offset);
        }

        @Override
        Vector3f calcScale(float elapsedTime) {
            final float loopDuration = 3.0f;
            return new Vector3f(mix(1.0f, 4.0f, calcLerpFactor(elapsedTime, loopDuration)));
        }
    }

    private class DynamicNonUniformScale extends Instance {
        DynamicNonUniformScale(Vector3f offset) {
            super(offset);
        }

        @Override
        Vector3f calcScale(float elapsedTime) {
            final float xLoopDuration = 3.0f;
            final float zLoopDuration = 5.0f;
            return new Vector3f(
                    mix(1.0f, 0.5f, calcLerpFactor(elapsedTime, xLoopDuration)),
                    1.0f,
                    mix(1.0f, 10.0f, calcLerpFactor(elapsedTime, zLoopDuration))
            );
        }
    }


    private float calcLerpFactor(float elapsedTime, float loopDuration) {
        float value = (elapsedTime % loopDuration) / loopDuration;
        if (value > 0.5f) {
            value = 1.0f - value;
        }
        return value * 2.0f;
    }


    private float mix(float x, float y, float a) {
        return x + a * (y - x);
    }
} public class Translation extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut06/data/";
        new Translation().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexBuffer();

        vao = glGenVertexArrays();
        glBindVertexArray(vao);

        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glEnableVertexAttribArray(0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 0, 0);
        final int numberOfVertices = 8;
        int colorDataOffset = Float.BYTES * 3 * numberOfVertices;
        glVertexAttribPointer(1, 4, GL_FLOAT, false, 0, colorDataOffset);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);

        glBindVertexArray(0);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        glUseProgram(theProgram);

        glBindVertexArray(vao);

        for (Instance currInst : instanceList) {
            final Matrix4f transformMatrix = currInst.constructMatrix(elapsedTime);
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, transformMatrix.get(mat4Buffer));
            glDrawElements(GL_TRIANGLES, indexData.length, GL_UNSIGNED_SHORT, 0);
        }

        glBindVertexArray(0);
        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale / (w / (float) h));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(45.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorPassthrough.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");

        float zNear = 1.0f;
        float zFar = 45.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private final float vertexData[] = {
            +1.0f, +1.0f, +1.0f,
            -1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, -1.0f,

            -1.0f, -1.0f, -1.0f,
            +1.0f, +1.0f, -1.0f,
            +1.0f, -1.0f, +1.0f,
            -1.0f, +1.0f, +1.0f,

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f,  // BROWN_COLOR

            0.0f, 1.0f, 0.0f, 1.0f,  // GREEN_COLOR
            0.0f, 0.0f, 1.0f, 1.0f,  // BLUE_COLOR
            1.0f, 0.0f, 0.0f, 1.0f,  // RED_COLOR
            0.5f, 0.5f, 0.0f, 1.0f   // BROWN_COLOR
    };

    private final short indexData[] = {
            0, 1, 2,
            1, 0, 3,
            2, 3, 0,
            3, 2, 1,

            5, 4, 6,
            4, 5, 7,
            7, 6, 4,
            6, 7, 5
    };

    private int vertexBufferObject;
    private int indexBufferObject;

    private int vao;


    private void initializeVertexBuffer() {
        FloatBuffer vertexDataBuffer = BufferUtils.createFloatBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        vertexBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, vertexBufferObject);
        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        ShortBuffer indexDataBuffer = BufferUtils.createShortBuffer(indexData.length);
        indexDataBuffer.put(indexData);
        indexDataBuffer.flip();

        indexBufferObject = glGenBuffers();
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBufferObject);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indexDataBuffer, GL_STATIC_DRAW);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private Instance instanceList[] = {
            new StationaryOffset(),
            new OvalOffset(),
            new BottomCircleOffset()
    };

    private abstract class Instance {
        abstract Vector3f calcOffset(float elapsedTime);

        Matrix4f constructMatrix(float elapsedTime) {
            Matrix4f theMat = new Matrix4f();

            theMat.setTranslation(calcOffset(elapsedTime));
            return theMat;
        }
    }

    private class StationaryOffset extends Instance {
        @Override
        Vector3f calcOffset(float elapsedTime) {
            return new Vector3f(0.0f, 0.0f, -20.0f);
        }
    }

    private class OvalOffset extends Instance {
        @Override
        Vector3f calcOffset(float elapsedTime) {
            final float loopDuration = 3.0f;
            final float scale = 3.14159f * 2.0f / loopDuration;
            final float currTimeThroughLoop = elapsedTime % loopDuration;
            return new Vector3f(
                    (float) (Math.cos(currTimeThroughLoop * scale) * 4.f),
                    (float) (Math.sin(currTimeThroughLoop * scale) * 6.f),
                    -20.0f
            );
        }
    }

    private class BottomCircleOffset extends Instance {
        @Override
        Vector3f calcOffset(float elapsedTime) {
            final float loopDuration = 12.0f;
            final float scale = 3.14159f * 2.0f / loopDuration;
            final float currTimeThroughLoop = elapsedTime % loopDuration;
            return new Vector3f(
                    (float) (Math.cos(currTimeThroughLoop * scale) * 5.f),
                    -3.5f,
                    (float) (Math.sin(currTimeThroughLoop * scale) * 5.f - 20.f)
            );
        }
    }
} public class WorldScene extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut07/data/";
        new WorldScene().start(700, 700);
    }


    @Override
    protected void init() {
        initializeProgram();

        coneMesh = new Mesh("UnitConeTint.xml");
        cylinderMesh = new Mesh("UnitCylinderTint.xml");
        cubeTintMesh = new Mesh("UnitCubeTint.xml");
        cubeColorMesh = new Mesh("UnitCubeColor.xml");
        planeMesh = new Mesh("UnitPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawLookatPoint = !drawLookatPoint;
                        System.out.printf("Target: %f, %f, %f\n", camTarget.x, camTarget.y, camTarget.z);
                        System.out.printf("Position: %f, %f, %f\n", sphereCamRelPos.x, sphereCamRelPos.y, sphereCamRelPos.z);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        {
            final Vector3f camPos = resolveCamPosition();

            MatrixStackf camMatrix = new MatrixStackf();
            camMatrix.mul(calcLookAtMatrix(camPos, camTarget, new Vector3f(0.0f, 1.0f, 0.0f)));

            glUseProgram(uniformColor.theProgram);
            glUniformMatrix4fv(uniformColor.worldToCameraMatrixUnif, false, camMatrix.get(mat4Buffer));
            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.worldToCameraMatrixUnif, false, camMatrix.get(mat4Buffer));
            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.worldToCameraMatrixUnif, false, camMatrix.get(mat4Buffer));
            glUseProgram(0);

            MatrixStackf modelMatrix = new MatrixStackf(4);

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();
                modelMatrix.scale(100.0f, 1.0f, 100.0f);

                glUseProgram(uniformColor.theProgram);
                glUniformMatrix4fv(uniformColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(uniformColor.baseColorUnif, 0.302f, 0.416f, 0.0589f, 1.0f);
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Draw the trees.
            drawForest(modelMatrix);

            // Draw the building.
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate(20.0f, 0.0f, -10.0f);

                drawParthenon(modelMatrix);

                modelMatrix.popMatrix();
            }

            if (drawLookatPoint) {
                glDisable(GL_DEPTH_TEST);

                modelMatrix.pushMatrix();
                Vector3f cameraAimVec = new Vector3f(camTarget).sub(camPos);
                modelMatrix.translate(0.0f, 0.0f, -cameraAimVec.length());
                modelMatrix.scale(1.0f, 1.0f, 1.0f);

                Matrix4f identity = new Matrix4f();

                glUseProgram(objectColor.theProgram);
                glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniformMatrix4fv(objectColor.worldToCameraMatrixUnif, false, identity.get(mat4Buffer));
                cubeColorMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();

                glEnable(GL_DEPTH_TEST);
            }
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        glUseProgram(uniformColor.theProgram);
        glUniformMatrix4fv(uniformColor.cameraToClipMatrixUnif, false, persMatrix.get(mat4Buffer));
        glUseProgram(objectColor.theProgram);
        glUniformMatrix4fv(objectColor.cameraToClipMatrixUnif, false, persMatrix.get(mat4Buffer));
        glUseProgram(uniformColorTint.theProgram);
        glUniformMatrix4fv(uniformColorTint.cameraToClipMatrixUnif, false, persMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_W)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.z = camTarget.z - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.z = camTarget.z - 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_S)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.z = camTarget.z + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.z = camTarget.z + 4.0f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.x = camTarget.x + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.x = camTarget.x + 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_A)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.x = camTarget.x - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.x = camTarget.x - 4.0f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.y = camTarget.y - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.y = camTarget.y - 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.y = camTarget.y + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.y = camTarget.y + 4.0f * lastFrameDuration * scale;
            }
        }


        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y + 11.25f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x + 11.25f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_O)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.z = sphereCamRelPos.z - 0.5f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.z = sphereCamRelPos.z - 5.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_U)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.z = sphereCamRelPos.z + 0.5f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.z = sphereCamRelPos.z + 5.0f * lastFrameDuration * scale;
            }
        }


        sphereCamRelPos.y = Math.min(Math.max(sphereCamRelPos.y, -78.75f), -1.0f);
        camTarget.y = camTarget.y > 0.0f ? camTarget.y : 0.0f;
        sphereCamRelPos.z = sphereCamRelPos.z > 5.0f ? sphereCamRelPos.z : 5.0f;
    }

    ////////////////////////////////
    private ProgramData uniformColor;
    private ProgramData objectColor;
    private ProgramData uniformColorTint;

    private class ProgramData {
        int theProgram;
        int modelToWorldMatrixUnif;
        int worldToCameraMatrixUnif;
        int cameraToClipMatrixUnif;
        int baseColorUnif;
    }


    private void initializeProgram() {
        uniformColor = loadProgram("PosOnlyWorldTransform.vert", "ColorUniform.frag");
        objectColor = loadProgram("PosColorWorldTransform.vert", "ColorPassthrough.frag");
        uniformColorTint = loadProgram("PosColorWorldTransform.vert", "ColorMultUniform.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToWorldMatrixUnif = glGetUniformLocation(data.theProgram, "modelToWorldMatrix");
        data.worldToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "worldToCameraMatrix");
        data.cameraToClipMatrixUnif = glGetUniformLocation(data.theProgram, "cameraToClipMatrix");
        data.baseColorUnif = glGetUniformLocation(data.theProgram, "baseColor");
        return data;
    }

    ////////////////////////////////
    private Mesh coneMesh;
    private Mesh cylinderMesh;
    private Mesh cubeTintMesh;
    private Mesh cubeColorMesh;
    private Mesh planeMesh;

    private boolean drawLookatPoint = false;
    private Vector3f camTarget = new Vector3f(0.0f, 0.4f, 0.0f);

    // In spherical coordinates.
    private Vector3f sphereCamRelPos = new Vector3f(67.5f, -46.0f, 150.0f);


    private Vector3f resolveCamPosition() {
        float phi = (float) Math.toRadians(sphereCamRelPos.x);
        float theta = (float) Math.toRadians(sphereCamRelPos.y + 90.0f);

        float sinTheta = (float) Math.sin(theta);
        float cosTheta = (float) Math.cos(theta);
        float cosPhi = (float) Math.cos(phi);
        float sinPhi = (float) Math.sin(phi);

        Vector3f dirToCamera = new Vector3f(sinTheta * cosPhi, cosTheta, sinTheta * sinPhi);
        return (dirToCamera.mul(sphereCamRelPos.z)).add(camTarget);
    }


    private Matrix4f calcLookAtMatrix(Vector3f cameraPt, Vector3f lookPt, Vector3f upPt) {
        Vector3f lookDir = new Vector3f(lookPt).sub(cameraPt).normalize();
        Vector3f upDir = new Vector3f(upPt).normalize();

        Vector3f rightDir = new Vector3f(lookDir).cross(upDir).normalize();
        Vector3f perpUpDir = new Vector3f(rightDir).cross(lookDir);

        Matrix4f rotMat = new Matrix4f();
        rotMat.m00(rightDir.x);
        rotMat.m01(rightDir.y);
        rotMat.m02(rightDir.z);
        rotMat.m10(perpUpDir.x);
        rotMat.m11(perpUpDir.y);
        rotMat.m12(perpUpDir.z);
        rotMat.m20(-lookDir.x);
        rotMat.m21(-lookDir.y);
        rotMat.m22(-lookDir.z);
        rotMat.transpose();

        Matrix4f transMat = new Matrix4f().setTranslation(-cameraPt.x, -cameraPt.y, -cameraPt.z);
        return rotMat.mul(transMat);
    }

    ////////////////////////////////
    private final TreeData forest[] = {
            new TreeData(-45.0f, -40.0f, 2.0f, 3.0f),
            new TreeData(-42.0f, -35.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, -29.0f, 2.0f, 4.0f),
            new TreeData(-44.0f, -26.0f, 3.0f, 3.0f),
            new TreeData(-40.0f, -22.0f, 2.0f, 4.0f),
            new TreeData(-36.0f, -15.0f, 3.0f, 3.0f),
            new TreeData(-41.0f, -11.0f, 2.0f, 3.0f),
            new TreeData(-37.0f, -6.0f, 3.0f, 3.0f),
            new TreeData(-45.0f, 0.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, 4.0f, 3.0f, 4.0f),
            new TreeData(-36.0f, 8.0f, 2.0f, 3.0f),
            new TreeData(-44.0f, 13.0f, 3.0f, 3.0f),
            new TreeData(-42.0f, 17.0f, 2.0f, 3.0f),
            new TreeData(-38.0f, 23.0f, 3.0f, 4.0f),
            new TreeData(-41.0f, 27.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, 32.0f, 3.0f, 3.0f),
            new TreeData(-44.0f, 37.0f, 3.0f, 4.0f),
            new TreeData(-36.0f, 42.0f, 2.0f, 3.0f),

            new TreeData(-32.0f, -45.0f, 2.0f, 3.0f),
            new TreeData(-30.0f, -42.0f, 2.0f, 4.0f),
            new TreeData(-34.0f, -38.0f, 3.0f, 5.0f),
            new TreeData(-33.0f, -35.0f, 3.0f, 4.0f),
            new TreeData(-29.0f, -28.0f, 2.0f, 3.0f),
            new TreeData(-26.0f, -25.0f, 3.0f, 5.0f),
            new TreeData(-35.0f, -21.0f, 3.0f, 4.0f),
            new TreeData(-31.0f, -17.0f, 3.0f, 3.0f),
            new TreeData(-28.0f, -12.0f, 2.0f, 4.0f),
            new TreeData(-29.0f, -7.0f, 3.0f, 3.0f),
            new TreeData(-26.0f, -1.0f, 2.0f, 4.0f),
            new TreeData(-32.0f, 6.0f, 2.0f, 3.0f),
            new TreeData(-30.0f, 10.0f, 3.0f, 5.0f),
            new TreeData(-33.0f, 14.0f, 2.0f, 4.0f),
            new TreeData(-35.0f, 19.0f, 3.0f, 4.0f),
            new TreeData(-28.0f, 22.0f, 2.0f, 3.0f),
            new TreeData(-33.0f, 26.0f, 3.0f, 3.0f),
            new TreeData(-29.0f, 31.0f, 3.0f, 4.0f),
            new TreeData(-32.0f, 38.0f, 2.0f, 3.0f),
            new TreeData(-27.0f, 41.0f, 3.0f, 4.0f),
            new TreeData(-31.0f, 45.0f, 2.0f, 4.0f),
            new TreeData(-28.0f, 48.0f, 3.0f, 5.0f),

            new TreeData(-25.0f, -48.0f, 2.0f, 3.0f),
            new TreeData(-20.0f, -42.0f, 3.0f, 4.0f),
            new TreeData(-22.0f, -39.0f, 2.0f, 3.0f),
            new TreeData(-19.0f, -34.0f, 2.0f, 3.0f),
            new TreeData(-23.0f, -30.0f, 3.0f, 4.0f),
            new TreeData(-24.0f, -24.0f, 2.0f, 3.0f),
            new TreeData(-16.0f, -21.0f, 2.0f, 3.0f),
            new TreeData(-17.0f, -17.0f, 3.0f, 3.0f),
            new TreeData(-25.0f, -13.0f, 2.0f, 4.0f),
            new TreeData(-23.0f, -8.0f, 2.0f, 3.0f),
            new TreeData(-17.0f, -2.0f, 3.0f, 3.0f),
            new TreeData(-16.0f, 1.0f, 2.0f, 3.0f),
            new TreeData(-19.0f, 4.0f, 3.0f, 3.0f),
            new TreeData(-22.0f, 8.0f, 2.0f, 4.0f),
            new TreeData(-21.0f, 14.0f, 2.0f, 3.0f),
            new TreeData(-16.0f, 19.0f, 2.0f, 3.0f),
            new TreeData(-23.0f, 24.0f, 3.0f, 3.0f),
            new TreeData(-18.0f, 28.0f, 2.0f, 4.0f),
            new TreeData(-24.0f, 31.0f, 2.0f, 3.0f),
            new TreeData(-20.0f, 36.0f, 2.0f, 3.0f),
            new TreeData(-22.0f, 41.0f, 3.0f, 3.0f),
            new TreeData(-21.0f, 45.0f, 2.0f, 3.0f),

            new TreeData(-12.0f, -40.0f, 2.0f, 4.0f),
            new TreeData(-11.0f, -35.0f, 3.0f, 3.0f),
            new TreeData(-10.0f, -29.0f, 1.0f, 3.0f),
            new TreeData(-9.0f, -26.0f, 2.0f, 2.0f),
            new TreeData(-6.0f, -22.0f, 2.0f, 3.0f),
            new TreeData(-15.0f, -15.0f, 1.0f, 3.0f),
            new TreeData(-8.0f, -11.0f, 2.0f, 3.0f),
            new TreeData(-14.0f, -6.0f, 2.0f, 4.0f),
            new TreeData(-12.0f, 0.0f, 2.0f, 3.0f),
            new TreeData(-7.0f, 4.0f, 2.0f, 2.0f),
            new TreeData(-13.0f, 8.0f, 2.0f, 2.0f),
            new TreeData(-9.0f, 13.0f, 1.0f, 3.0f),
            new TreeData(-13.0f, 17.0f, 3.0f, 4.0f),
            new TreeData(-6.0f, 23.0f, 2.0f, 3.0f),
            new TreeData(-12.0f, 27.0f, 1.0f, 2.0f),
            new TreeData(-8.0f, 32.0f, 2.0f, 3.0f),
            new TreeData(-10.0f, 37.0f, 3.0f, 3.0f),
            new TreeData(-11.0f, 42.0f, 2.0f, 2.0f),

            new TreeData(15.0f, 5.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 10.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 15.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 20.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 25.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 30.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 35.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 40.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 45.0f, 2.0f, 3.0f),

            new TreeData(25.0f, 5.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 10.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 15.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 20.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 25.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 30.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 35.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 40.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 45.0f, 2.0f, 3.0f)
    };

    private class TreeData {
        float xPos;
        float zPos;
        float trunkHeight;
        float coneHeight;

        TreeData(float xPos, float zPos, float trunkHeight, float coneHeight) {
            this.xPos = xPos;
            this.zPos = zPos;
            this.trunkHeight = trunkHeight;
            this.coneHeight = coneHeight;
        }
    }


    private void drawForest(MatrixStackf modelMatrix) {
        for (TreeData currTree : forest) {
            modelMatrix.pushMatrix();
            modelMatrix.translate(currTree.xPos, 0.0f, currTree.zPos);

            drawTree(modelMatrix, currTree.trunkHeight, currTree.coneHeight);

            modelMatrix.popMatrix();
        }
    }

    // Trees are 3x3 in X/Z, and trunkHeight + coneHeight in the Y.
    private void drawTree(MatrixStackf modelMatrix, float trunkHeight, float coneHeight) {
        // Draw trunk.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(1.0f, trunkHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.694f, 0.4f, 0.106f, 1.0f);
            cylinderMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the treetop.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, trunkHeight, 0.0f);
            modelMatrix.scale(3.0f, coneHeight, 3.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.0f, 1.0f, 0.0f, 1.0f);
            coneMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }

    ////////////////////////////////
    // Columns are 1x1 in the X/Z, and height units in the Y.
    private void drawColumn(MatrixStackf modelMatrix, float height) {
        final float columnBaseHeight = 0.25f;

        // Draw the bottom of the column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(1.0f, columnBaseHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the top of the column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, height - columnBaseHeight, 0.0f);
            modelMatrix.scale(1.0f, columnBaseHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the main column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, columnBaseHeight, 0.0f);
            modelMatrix.scale(0.8f, height - (columnBaseHeight * 2.0f), 0.8f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cylinderMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }

    ////////////////////////////////
    private void drawParthenon(MatrixStackf modelMatrix) {
        final float parthenonWidth = 14.0f;
        final float parthenonLength = 20.0f;
        final float parthenonColumnHeight = 5.0f;
        final float parthenonBaseHeight = 1.0f;
        final float parthenonTopHeight = 2.0f;

        // Draw base.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(parthenonWidth, parthenonBaseHeight, parthenonLength);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw top.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, parthenonColumnHeight + parthenonBaseHeight, 0.0f);
            modelMatrix.scale(parthenonWidth, parthenonTopHeight, parthenonLength);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw columns.
        final float frontZVal = (parthenonLength / 2.0f) - 1.0f;
        final float rightXVal = (parthenonWidth / 2.0f) - 1.0f;
        for (int columnNum = 0; columnNum < (int) (parthenonWidth / 2.0f); columnNum++) {
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate((2.0f * columnNum) - (parthenonWidth / 2.0f) + 1.0f, parthenonBaseHeight, frontZVal);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate((2.0f * columnNum) - (parthenonWidth / 2.0f) + 1.0f, parthenonBaseHeight, -frontZVal);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
        }

        // Don't draw the first or last columns, since they've been drawn already.
        for (int columnNum = 1; columnNum < (int) ((parthenonLength - 2.0f) / 2.0f); columnNum++) {
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate(rightXVal, parthenonBaseHeight, (2.0f * columnNum) - (parthenonLength / 2.0f) + 1.0f);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate(-rightXVal, parthenonBaseHeight, (2.0f * columnNum) - (parthenonLength / 2.0f) + 1.0f);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
        }

        // Draw interior.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, 1.0f, 0.0f);
            modelMatrix.scale(parthenonWidth - 6.0f, parthenonColumnHeight, parthenonLength - 6.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeColorMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw headpiece.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, parthenonColumnHeight + parthenonBaseHeight + (parthenonTopHeight / 2.0f), parthenonLength / 2.0f);
            modelMatrix.rotateX(-135.0f);
            modelMatrix.rotateY(45.0f);

            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeColorMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }
} public class WorldWithUBO extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut07/data/";
        new WorldWithUBO().start(700, 700);
    }


    @Override
    protected void init() {
        initializeProgram();

        coneMesh = new Mesh("UnitConeTint.xml");
        cylinderMesh = new Mesh("UnitCylinderTint.xml");
        cubeTintMesh = new Mesh("UnitCubeTint.xml");
        cubeColorMesh = new Mesh("UnitCubeColor.xml");
        planeMesh = new Mesh("UnitPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawLookatPoint = !drawLookatPoint;
                        System.out.printf("Target: %f, %f, %f\n", camTarget.x, camTarget.y, camTarget.z);
                        System.out.printf("Position: %f, %f, %f\n", sphereCamRelPos.x, sphereCamRelPos.y, sphereCamRelPos.z);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        final Vector3f camPos = resolveCamPosition();

        MatrixStackf camMatrix = new MatrixStackf();
        camMatrix.mul(calcLookAtMatrix(camPos, camTarget, new Vector3f(0.0f, 1.0f, 0.0f)));

        glBindBuffer(GL_UNIFORM_BUFFER, globalMatricesUBO);
        glBufferSubData(GL_UNIFORM_BUFFER, 16 * 4, camMatrix.get(mat4Buffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        MatrixStackf modelMatrix = new MatrixStackf(4);

        // Render the ground plane.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(100.0f, 1.0f, 100.0f);

            glUseProgram(uniformColor.theProgram);
            glUniformMatrix4fv(uniformColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColor.baseColorUnif, 0.302f, 0.416f, 0.0589f, 1.0f);
            planeMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the trees.
        drawForest(modelMatrix);

        // Draw the building.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(20.0f, 0.0f, -10.0f);

            drawParthenon(modelMatrix);

            modelMatrix.popMatrix();
        }

        if (drawLookatPoint) {
            glDisable(GL_DEPTH_TEST);

            modelMatrix.pushMatrix();
            modelMatrix.translate(camTarget);
            modelMatrix.scale(1.0f, 1.0f, 1.0f);

            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeColorMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();

            glEnable(GL_DEPTH_TEST);
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        glBindBuffer(GL_UNIFORM_BUFFER, globalMatricesUBO);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, persMatrix.get(mat4Buffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_W)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.z = camTarget.z - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.z = camTarget.z - 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_S)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.z = camTarget.z + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.z = camTarget.z + 4.0f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.x = camTarget.x + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.x = camTarget.x + 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_A)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.x = camTarget.x - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.x = camTarget.x - 4.0f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.y = camTarget.y - 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.y = camTarget.y - 4.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                camTarget.y = camTarget.y + 0.4f * lastFrameDuration * scale;
            } else {
                camTarget.y = camTarget.y + 4.0f * lastFrameDuration * scale;
            }
        }


        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y + 11.25f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x + 11.25f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_O)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.z = sphereCamRelPos.z - 0.5f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.z = sphereCamRelPos.z - 5.0f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_U)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.z = sphereCamRelPos.z + 0.5f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.z = sphereCamRelPos.z + 5.0f * lastFrameDuration * scale;
            }
        }


        sphereCamRelPos.y = Math.min(Math.max(sphereCamRelPos.y, -78.75f), -1.0f);
        camTarget.y = camTarget.y > 0.0f ? camTarget.y : 0.0f;
        sphereCamRelPos.z = sphereCamRelPos.z > 5.0f ? sphereCamRelPos.z : 5.0f;
    }

    ////////////////////////////////
    private ProgramData uniformColor;
    private ProgramData objectColor;
    private ProgramData uniformColorTint;

    private class ProgramData {
        int theProgram;
        int globalUniformBlockIndex;
        int modelToWorldMatrixUnif;
        int baseColorUnif;
    }

    private int globalMatricesUBO;

    private final int globalMatricesBindingIndex = 0;


    private void initializeProgram() {
        uniformColor = loadProgram("PosOnlyWorldTransformUBO.vert", "ColorUniform.frag");
        objectColor = loadProgram("PosColorWorldTransformUBO.vert", "ColorPassthrough.frag");
        uniformColorTint = loadProgram("PosColorWorldTransformUBO.vert", "ColorMultUniform.frag");

        globalMatricesUBO = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, globalMatricesUBO);
        glBufferData(GL_UNIFORM_BUFFER, 16 * 4 * 2, GL_STREAM_DRAW);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);
        glBindBufferRange(GL_UNIFORM_BUFFER, globalMatricesBindingIndex, globalMatricesUBO, 0, 16 * 4 * 2);
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToWorldMatrixUnif = glGetUniformLocation(data.theProgram, "modelToWorldMatrix");
        data.globalUniformBlockIndex = glGetUniformBlockIndex(data.theProgram, "GlobalMatrices");
        data.baseColorUnif = glGetUniformLocation(data.theProgram, "baseColor");

        glUniformBlockBinding(data.theProgram, data.globalUniformBlockIndex, globalMatricesBindingIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh coneMesh;
    private Mesh cylinderMesh;
    private Mesh cubeTintMesh;
    private Mesh cubeColorMesh;
    private Mesh planeMesh;

    private boolean drawLookatPoint = false;
    private Vector3f camTarget = new Vector3f(0.0f, 0.4f, 0.0f);

    // In spherical coordinates.
    private Vector3f sphereCamRelPos = new Vector3f(67.5f, -46.0f, 150.0f);


    private Vector3f resolveCamPosition() {
        float phi = (float) Math.toRadians(sphereCamRelPos.x);
        float theta = (float) Math.toRadians(sphereCamRelPos.y + 90.0f);

        float sinTheta = (float) Math.sin(theta);
        float cosTheta = (float) Math.cos(theta);
        float cosPhi = (float) Math.cos(phi);
        float sinPhi = (float) Math.sin(phi);

        Vector3f dirToCamera = new Vector3f(sinTheta * cosPhi, cosTheta, sinTheta * sinPhi);
        return (dirToCamera.mul(sphereCamRelPos.z)).add(camTarget);
    }


    private Matrix4f calcLookAtMatrix(Vector3f cameraPt, Vector3f lookPt, Vector3f upPt) {
        Vector3f lookDir = new Vector3f(lookPt).sub(cameraPt).normalize();
        Vector3f upDir = new Vector3f(upPt).normalize();

        Vector3f rightDir = new Vector3f(lookDir).cross(upDir).normalize();
        Vector3f perpUpDir = new Vector3f(rightDir).cross(lookDir);

        Matrix4f rotMat = new Matrix4f();
        rotMat.m00(rightDir.x);
        rotMat.m01(rightDir.y);
        rotMat.m02(rightDir.z);
        rotMat.m10(perpUpDir.x);
        rotMat.m11(perpUpDir.y);
        rotMat.m12(perpUpDir.z);
        rotMat.m20(-lookDir.x);
        rotMat.m21(-lookDir.y);
        rotMat.m22(-lookDir.z);
        rotMat.transpose();

        Matrix4f transMat = new Matrix4f().setTranslation(-cameraPt.x, -cameraPt.y, -cameraPt.z);
        return rotMat.mul(transMat);
    }

    ////////////////////////////////
    private final TreeData forest[] = {
            new TreeData(-45.0f, -40.0f, 2.0f, 3.0f),
            new TreeData(-42.0f, -35.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, -29.0f, 2.0f, 4.0f),
            new TreeData(-44.0f, -26.0f, 3.0f, 3.0f),
            new TreeData(-40.0f, -22.0f, 2.0f, 4.0f),
            new TreeData(-36.0f, -15.0f, 3.0f, 3.0f),
            new TreeData(-41.0f, -11.0f, 2.0f, 3.0f),
            new TreeData(-37.0f, -6.0f, 3.0f, 3.0f),
            new TreeData(-45.0f, 0.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, 4.0f, 3.0f, 4.0f),
            new TreeData(-36.0f, 8.0f, 2.0f, 3.0f),
            new TreeData(-44.0f, 13.0f, 3.0f, 3.0f),
            new TreeData(-42.0f, 17.0f, 2.0f, 3.0f),
            new TreeData(-38.0f, 23.0f, 3.0f, 4.0f),
            new TreeData(-41.0f, 27.0f, 2.0f, 3.0f),
            new TreeData(-39.0f, 32.0f, 3.0f, 3.0f),
            new TreeData(-44.0f, 37.0f, 3.0f, 4.0f),
            new TreeData(-36.0f, 42.0f, 2.0f, 3.0f),

            new TreeData(-32.0f, -45.0f, 2.0f, 3.0f),
            new TreeData(-30.0f, -42.0f, 2.0f, 4.0f),
            new TreeData(-34.0f, -38.0f, 3.0f, 5.0f),
            new TreeData(-33.0f, -35.0f, 3.0f, 4.0f),
            new TreeData(-29.0f, -28.0f, 2.0f, 3.0f),
            new TreeData(-26.0f, -25.0f, 3.0f, 5.0f),
            new TreeData(-35.0f, -21.0f, 3.0f, 4.0f),
            new TreeData(-31.0f, -17.0f, 3.0f, 3.0f),
            new TreeData(-28.0f, -12.0f, 2.0f, 4.0f),
            new TreeData(-29.0f, -7.0f, 3.0f, 3.0f),
            new TreeData(-26.0f, -1.0f, 2.0f, 4.0f),
            new TreeData(-32.0f, 6.0f, 2.0f, 3.0f),
            new TreeData(-30.0f, 10.0f, 3.0f, 5.0f),
            new TreeData(-33.0f, 14.0f, 2.0f, 4.0f),
            new TreeData(-35.0f, 19.0f, 3.0f, 4.0f),
            new TreeData(-28.0f, 22.0f, 2.0f, 3.0f),
            new TreeData(-33.0f, 26.0f, 3.0f, 3.0f),
            new TreeData(-29.0f, 31.0f, 3.0f, 4.0f),
            new TreeData(-32.0f, 38.0f, 2.0f, 3.0f),
            new TreeData(-27.0f, 41.0f, 3.0f, 4.0f),
            new TreeData(-31.0f, 45.0f, 2.0f, 4.0f),
            new TreeData(-28.0f, 48.0f, 3.0f, 5.0f),

            new TreeData(-25.0f, -48.0f, 2.0f, 3.0f),
            new TreeData(-20.0f, -42.0f, 3.0f, 4.0f),
            new TreeData(-22.0f, -39.0f, 2.0f, 3.0f),
            new TreeData(-19.0f, -34.0f, 2.0f, 3.0f),
            new TreeData(-23.0f, -30.0f, 3.0f, 4.0f),
            new TreeData(-24.0f, -24.0f, 2.0f, 3.0f),
            new TreeData(-16.0f, -21.0f, 2.0f, 3.0f),
            new TreeData(-17.0f, -17.0f, 3.0f, 3.0f),
            new TreeData(-25.0f, -13.0f, 2.0f, 4.0f),
            new TreeData(-23.0f, -8.0f, 2.0f, 3.0f),
            new TreeData(-17.0f, -2.0f, 3.0f, 3.0f),
            new TreeData(-16.0f, 1.0f, 2.0f, 3.0f),
            new TreeData(-19.0f, 4.0f, 3.0f, 3.0f),
            new TreeData(-22.0f, 8.0f, 2.0f, 4.0f),
            new TreeData(-21.0f, 14.0f, 2.0f, 3.0f),
            new TreeData(-16.0f, 19.0f, 2.0f, 3.0f),
            new TreeData(-23.0f, 24.0f, 3.0f, 3.0f),
            new TreeData(-18.0f, 28.0f, 2.0f, 4.0f),
            new TreeData(-24.0f, 31.0f, 2.0f, 3.0f),
            new TreeData(-20.0f, 36.0f, 2.0f, 3.0f),
            new TreeData(-22.0f, 41.0f, 3.0f, 3.0f),
            new TreeData(-21.0f, 45.0f, 2.0f, 3.0f),

            new TreeData(-12.0f, -40.0f, 2.0f, 4.0f),
            new TreeData(-11.0f, -35.0f, 3.0f, 3.0f),
            new TreeData(-10.0f, -29.0f, 1.0f, 3.0f),
            new TreeData(-9.0f, -26.0f, 2.0f, 2.0f),
            new TreeData(-6.0f, -22.0f, 2.0f, 3.0f),
            new TreeData(-15.0f, -15.0f, 1.0f, 3.0f),
            new TreeData(-8.0f, -11.0f, 2.0f, 3.0f),
            new TreeData(-14.0f, -6.0f, 2.0f, 4.0f),
            new TreeData(-12.0f, 0.0f, 2.0f, 3.0f),
            new TreeData(-7.0f, 4.0f, 2.0f, 2.0f),
            new TreeData(-13.0f, 8.0f, 2.0f, 2.0f),
            new TreeData(-9.0f, 13.0f, 1.0f, 3.0f),
            new TreeData(-13.0f, 17.0f, 3.0f, 4.0f),
            new TreeData(-6.0f, 23.0f, 2.0f, 3.0f),
            new TreeData(-12.0f, 27.0f, 1.0f, 2.0f),
            new TreeData(-8.0f, 32.0f, 2.0f, 3.0f),
            new TreeData(-10.0f, 37.0f, 3.0f, 3.0f),
            new TreeData(-11.0f, 42.0f, 2.0f, 2.0f),

            new TreeData(15.0f, 5.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 10.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 15.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 20.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 25.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 30.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 35.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 40.0f, 2.0f, 3.0f),
            new TreeData(15.0f, 45.0f, 2.0f, 3.0f),

            new TreeData(25.0f, 5.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 10.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 15.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 20.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 25.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 30.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 35.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 40.0f, 2.0f, 3.0f),
            new TreeData(25.0f, 45.0f, 2.0f, 3.0f)
    };

    private class TreeData {
        float xPos;
        float zPos;
        float trunkHeight;
        float coneHeight;

        TreeData(float xPos, float zPos, float trunkHeight, float coneHeight) {
            this.xPos = xPos;
            this.zPos = zPos;
            this.trunkHeight = trunkHeight;
            this.coneHeight = coneHeight;
        }
    }


    private void drawForest(MatrixStackf modelMatrix) {
        for (TreeData currTree : forest) {
            modelMatrix.pushMatrix();
            modelMatrix.translate(currTree.xPos, 0.0f, currTree.zPos);

            drawTree(modelMatrix, currTree.trunkHeight, currTree.coneHeight);

            modelMatrix.popMatrix();
        }
    }

    // Trees are 3x3 in X/Z, and trunkHeight + coneHeight in the Y.
    private void drawTree(MatrixStackf modelMatrix, float trunkHeight, float coneHeight) {
        // Draw trunk.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(1.0f, trunkHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.694f, 0.4f, 0.106f, 1.0f);
            cylinderMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the treetop.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, trunkHeight, 0.0f);
            modelMatrix.scale(3.0f, coneHeight, 3.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.0f, 1.0f, 0.0f, 1.0f);
            coneMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }

    ////////////////////////////////
    // Columns are 1x1 in the X/Z, and height units in the Y.
    private void drawColumn(MatrixStackf modelMatrix, float height) {
        final float columnBaseHeight = 0.25f;

        // Draw the bottom of the column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(1.0f, columnBaseHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the top of the column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, height - columnBaseHeight, 0.0f);
            modelMatrix.scale(1.0f, columnBaseHeight, 1.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw the main column.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, columnBaseHeight, 0.0f);
            modelMatrix.scale(0.8f, height - (columnBaseHeight * 2.0f), 0.8f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cylinderMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }

    ////////////////////////////////
    private void drawParthenon(MatrixStackf modelMatrix) {
        final float parthenonWidth = 14.0f;
        final float parthenonLength = 20.0f;
        final float parthenonColumnHeight = 5.0f;
        final float parthenonBaseHeight = 1.0f;
        final float parthenonTopHeight = 2.0f;

        // Draw base.
        {
            modelMatrix.pushMatrix();
            modelMatrix.scale(parthenonWidth, parthenonBaseHeight, parthenonLength);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw top.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, parthenonColumnHeight + parthenonBaseHeight, 0.0f);
            modelMatrix.scale(parthenonWidth, parthenonTopHeight, parthenonLength);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(uniformColorTint.theProgram);
            glUniformMatrix4fv(uniformColorTint.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(uniformColorTint.baseColorUnif, 0.9f, 0.9f, 0.9f, 0.9f);
            cubeTintMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw columns.
        final float frontZVal = (parthenonLength / 2.0f) - 1.0f;
        final float rightXVal = (parthenonWidth / 2.0f) - 1.0f;
        for (int columnNum = 0; columnNum < (int) (parthenonWidth / 2.0f); columnNum++) {
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate((2.0f * columnNum) - (parthenonWidth / 2.0f) + 1.0f, parthenonBaseHeight, frontZVal);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate((2.0f * columnNum) - (parthenonWidth / 2.0f) + 1.0f, parthenonBaseHeight, -frontZVal);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
        }

        // Don't draw the first or last columns, since they've been drawn already.
        for (int columnNum = 1; columnNum < (int) ((parthenonLength - 2.0f) / 2.0f); columnNum++) {
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate(rightXVal, parthenonBaseHeight, (2.0f * columnNum) - (parthenonLength / 2.0f) + 1.0f);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
            {
                modelMatrix.pushMatrix();
                modelMatrix.translate(-rightXVal, parthenonBaseHeight, (2.0f * columnNum) - (parthenonLength / 2.0f) + 1.0f);

                drawColumn(modelMatrix, parthenonColumnHeight);

                modelMatrix.popMatrix();
            }
        }

        // Draw interior.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, 1.0f, 0.0f);
            modelMatrix.scale(parthenonWidth - 6.0f, parthenonColumnHeight, parthenonLength - 6.0f);
            modelMatrix.translate(0.0f, 0.5f, 0.0f);

            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeColorMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        // Draw headpiece.
        {
            modelMatrix.pushMatrix();
            modelMatrix.translate(0.0f, parthenonColumnHeight + parthenonBaseHeight + (parthenonTopHeight / 2.0f), parthenonLength / 2.0f);
            modelMatrix.rotateX(-135.0f);
            modelMatrix.rotateY(45.0f);

            glUseProgram(objectColor.theProgram);
            glUniformMatrix4fv(objectColor.modelToWorldMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeColorMesh.render();
            glUseProgram(0);

            modelMatrix.popMatrix();
        }
    }
} public class CameraRelative extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut08/data/";
        new CameraRelative().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        ship = new Mesh("Ship.xml");
        plane = new Mesh("UnitPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        int ordinal = (offsetRelative.ordinal() + 1) % OffsetRelative.NUM_RELATIVES.ordinal();
                        offsetRelative = OffsetRelative.values()[ordinal];
                        switch (offsetRelative) {
                            case MODEL_RELATIVE:
                                System.out.printf("Model Relative\n");
                                break;

                            case WORLD_RELATIVE:
                                System.out.printf("World Relative\n");
                                break;

                            case CAMERA_RELATIVE:
                                System.out.printf("Camera Relative\n");
                                break;
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf currMatrix = new MatrixStackf(10);
        final Vector3f camPos = resolveCamPosition();
        currMatrix.mul(calcLookAtMatrix(camPos, camTarget, new Vector3f(0.0f, 1.0f, 0.0f)));

        glUseProgram(theProgram);

        {
            currMatrix.pushMatrix();
            currMatrix.scale(100.0f, 1.0f, 100.0f);

            glUniform4f(baseColorUnif, 0.2f, 0.5f, 0.2f, 1.0f);
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

            plane.render();

            currMatrix.popMatrix();
        }

        {
            currMatrix.pushMatrix();
            currMatrix.translate(camTarget);
            currMatrix.mul(orientation.get(new Matrix4f()));
            currMatrix.rotateX((float) Math.toRadians(-90.0f));

            // Set the base color for this object.
            glUniform4f(baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

            ship.render("tint");

            currMatrix.popMatrix();
        }

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale * (h / (float) w));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float SMALL_ANGLE_INCREMENT = 9.0f;
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            offsetOrientation(new Vector3f(1.0f, 0.0f, 0.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            offsetOrientation(new Vector3f(1.0f, 0.0f, 0.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_A)) {
            offsetOrientation(new Vector3f(0.0f, 0.0f, 1.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_D)) {
            offsetOrientation(new Vector3f(0.0f, 0.0f, 1.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_Q)) {
            offsetOrientation(new Vector3f(0.0f, 1.0f, 0.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_E)) {
            offsetOrientation(new Vector3f(0.0f, 1.0f, 0.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }


        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.y = sphereCamRelPos.y + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.y = sphereCamRelPos.y + 11.25f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x - 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x - 11.25f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                sphereCamRelPos.x = sphereCamRelPos.x + 1.125f * lastFrameDuration * scale;
            } else {
                sphereCamRelPos.x = sphereCamRelPos.x + 11.25f * lastFrameDuration * scale;
            }
        }


        sphereCamRelPos.y = Math.min(Math.max(sphereCamRelPos.y, -78.75f), 10.0f);
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;
    private int baseColorUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(20.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorMultUniform.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");
        baseColorUnif = glGetUniformLocation(theProgram, "baseColor");

        float zNear = 1.0f;
        float zFar = 600.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private Mesh ship;
    private Mesh plane;

    private Vector3f camTarget = new Vector3f(0.0f, 10.0f, 0.0f);
    private Quaternionf orientation = new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f);
    private OffsetRelative offsetRelative = OffsetRelative.MODEL_RELATIVE;

    // In spherical coordinates.
    private Vector3f sphereCamRelPos = new Vector3f(90.0f, 0.0f, 66.0f);

    private enum OffsetRelative {
        MODEL_RELATIVE,
        WORLD_RELATIVE,
        CAMERA_RELATIVE,

        NUM_RELATIVES
    }


    private void offsetOrientation(Vector3f axis, float angDeg) {
        float angRad = (float) Math.toRadians(angDeg);

        axis = new Vector3f(axis).normalize();
        axis.mul((float) Math.sin(angRad / 2.0f));

        float scalar = (float) Math.cos(angRad / 2.0f);
        Quaternionf offset = new Quaternionf(axis.x, axis.y, axis.z, scalar);

        switch (offsetRelative) {
            case MODEL_RELATIVE:
                orientation = new Quaternionf(orientation).mul(offset);
                break;

            case WORLD_RELATIVE:
                orientation = new Quaternionf(offset).mul(orientation);
                break;

            case CAMERA_RELATIVE:
                final Vector3f camPos = resolveCamPosition();
                final Matrix4f camMat = calcLookAtMatrix(camPos, camTarget, new Vector3f(0.0f, 1.0f, 0.0f));

                Quaternionf viewQuat = new Quaternionf();
                camMat.getNormalizedRotation(viewQuat);
                Quaternionf invViewQuat = new Quaternionf(viewQuat).conjugate();

                final Quaternionf worldQuat = invViewQuat.mul(offset.mul(viewQuat));
                orientation = new Quaternionf(worldQuat).mul(orientation);
                break;
        }

        orientation.normalize();
    }

    private Vector3f resolveCamPosition() {
        float phi = (float) Math.toRadians(sphereCamRelPos.x);
        float theta = (float) Math.toRadians(sphereCamRelPos.y + 90.0f);

        float sinTheta = (float) Math.sin(theta);
        float cosTheta = (float) Math.cos(theta);
        float cosPhi = (float) Math.cos(phi);
        float sinPhi = (float) Math.sin(phi);

        Vector3f dirToCamera = new Vector3f(sinTheta * cosPhi, cosTheta, sinTheta * sinPhi);
        return (dirToCamera.mul(sphereCamRelPos.z)).add(camTarget);
    }

    private Matrix4f calcLookAtMatrix(Vector3f cameraPt, Vector3f lookPt, Vector3f upPt) {
        Vector3f lookDir = new Vector3f(lookPt).sub(cameraPt).normalize();
        Vector3f upDir = new Vector3f(upPt).normalize();

        Vector3f rightDir = new Vector3f(lookDir).cross(upDir).normalize();
        Vector3f perpUpDir = new Vector3f(rightDir).cross(lookDir);

        Matrix4f rotMat = new Matrix4f();
        rotMat.m00(rightDir.x);
        rotMat.m01(rightDir.y);
        rotMat.m02(rightDir.z);
        rotMat.m10(perpUpDir.x);
        rotMat.m11(perpUpDir.y);
        rotMat.m12(perpUpDir.z);
        rotMat.m20(-lookDir.x);
        rotMat.m21(-lookDir.y);
        rotMat.m22(-lookDir.z);
        rotMat.transpose();

        Matrix4f transMat = new Matrix4f().setTranslation(-cameraPt.x, -cameraPt.y, -cameraPt.z);
        return rotMat.mul(transMat);
    }
} public class GimbalLock extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut08/data/";
        new GimbalLock().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        for (int gimbalIndex = 0; gimbalIndex < 3; gimbalIndex++) {
            gimbals[gimbalIndex] = new Mesh(gimbalNames[gimbalIndex]);
        }

        object = new Mesh("Ship.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawGimbals = !drawGimbals;
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf currMatrix = new MatrixStackf(10);
        currMatrix.translate(0.0f, 0.0f, -200.0f);
        currMatrix.rotateX((float) Math.toRadians(gimbalAngles.angleX));
        drawGimbal(currMatrix, GimbalAxis.X_AXIS, new Vector4f(0.4f, 0.4f, 1.0f, 1.0f));
        currMatrix.rotateY((float) Math.toRadians(gimbalAngles.angleY));
        drawGimbal(currMatrix, GimbalAxis.Y_AXIS, new Vector4f(0.0f, 1.0f, 0.0f, 1.0f));
        currMatrix.rotateZ((float) Math.toRadians(gimbalAngles.angleZ));
        drawGimbal(currMatrix, GimbalAxis.Z_AXIS, new Vector4f(1.0f, 0.3f, 0.3f, 1.0f));

        glUseProgram(theProgram);

        currMatrix.scale(3.0f, 3.0f, 3.0f);
        currMatrix.rotateX((float) Math.toRadians(-90.0f));

        // Set the base color for this object.
        glUniform4f(baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
        glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

        object.render("tint");

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale * (h / (float) w));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float SMALL_ANGLE_INCREMENT = 9.0f;
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            gimbalAngles.angleX += SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        } else if (isKeyPressed(GLFW_KEY_S)) {
            gimbalAngles.angleX -= SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        }

        if (isKeyPressed(GLFW_KEY_A)) {
            gimbalAngles.angleY += SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        } else if (isKeyPressed(GLFW_KEY_D)) {
            gimbalAngles.angleY -= SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        }

        if (isKeyPressed(GLFW_KEY_Q)) {
            gimbalAngles.angleZ += SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        } else if (isKeyPressed(GLFW_KEY_E)) {
            gimbalAngles.angleZ -= SMALL_ANGLE_INCREMENT * lastFrameDuration * scale;
        }
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;
    private int baseColorUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(20.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorMultUniform.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");
        baseColorUnif = glGetUniformLocation(theProgram, "baseColor");

        float zNear = 1.0f;
        float zFar = 600.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private Mesh object;

    private Mesh gimbals[] = new Mesh[3];
    private final String gimbalNames[] = {
            "LargeGimbal.xml",
            "MediumGimbal.xml",
            "SmallGimbal.xml"
    };
    private GimbalAngles gimbalAngles = new GimbalAngles();

    private boolean drawGimbals = true;

    private enum GimbalAxis {
        X_AXIS,
        Y_AXIS,
        Z_AXIS
    }

    private class GimbalAngles {
        float angleX;
        float angleY;
        float angleZ;
    }


    private void drawGimbal(MatrixStackf currMatrix, GimbalAxis axis, Vector4f baseColor) {
        if (!drawGimbals) return;

        currMatrix.pushMatrix();

        switch (axis) {
            case X_AXIS:
                break;

            case Y_AXIS:
                currMatrix.rotateZ((float) Math.toRadians(90.0f));
                currMatrix.rotateX((float) Math.toRadians(90.0f));
                break;

            case Z_AXIS:
                currMatrix.rotateY((float) Math.toRadians(90.0f));
                currMatrix.rotateX((float) Math.toRadians(90.0f));
                break;
        }

        glUseProgram(theProgram);

        // Set the base color for this object.
        glUniform4f(baseColorUnif, baseColor.x, baseColor.y, baseColor.z, baseColor.w);
        glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

        gimbals[axis.ordinal()].render();

        glUseProgram(0);

        currMatrix.popMatrix();
    }
} public class Interpolation extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut08/data/";
        new Interpolation().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        ship = new Mesh("Ship.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                for (int orientIndex = 0; orientIndex < orientKeys.length; orientIndex++) {
                    if (key == orientKeys[orientIndex]) {
                        applyOrientation(orientIndex);
                        break;
                    }
                }

                switch (key) {
                    case GLFW_KEY_SPACE:
                        boolean slerp = orient.toggleSlerp();
                        System.out.printf(slerp ? "Slerp\n" : "Lerp\n");
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        orient.updateTime();

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf currMatrix = new MatrixStackf();
        currMatrix.translate(0.0f, 0.0f, -200.0f);
        currMatrix.mul(orient.getOrient().get(new Matrix4f()));

        glUseProgram(theProgram);

        currMatrix.scale(3.0f, 3.0f, 3.0f);
        currMatrix.rotateX((float) Math.toRadians(-90.0f));

        // Set the base color for this object.
        glUniform4f(baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
        glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

        ship.render("tint");

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale * (h / (float) w));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;
    private int baseColorUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(20.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorMultUniform.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");
        baseColorUnif = glGetUniformLocation(theProgram, "baseColor");

        float zNear = 1.0f;
        float zFar = 600.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private Mesh ship;

    private Quaternionf orients[] = {
            new Quaternionf(0.7071f, 0.0f, 0.0f, 0.7071f),
            new Quaternionf(0.5f, -0.5f, 0.5f, 0.5f),
            new Quaternionf(-0.7892f, -0.3700f, -0.02514f, -0.4895f),
            new Quaternionf(0.7892f, 0.3700f, 0.02514f, 0.4895f),

            new Quaternionf(-0.1591f, -0.7991f, -0.4344f, 0.3840f),
            new Quaternionf(0.5208f, 0.6483f, 0.0410f, 0.5537f),
            new Quaternionf(0.0f, 1.0f, 0.0f, 0.0f)
    };

    private int orientKeys[] = {
            GLFW_KEY_Q,
            GLFW_KEY_W,
            GLFW_KEY_E,
            GLFW_KEY_R,

            GLFW_KEY_T,
            GLFW_KEY_Y,
            GLFW_KEY_U
    };

    private Orientation orient = new Orientation();

    private class Orientation {
        boolean isAnimating;
        boolean slerp;
        int currOrientIndex;

        Animation anim = new Animation();

        class Animation {
            int finalOrientIndex;
            Timer currTimer;


            boolean updateTime() {
                return currTimer.update(elapsedTime);
            }

            void startAnimation(int destinationIndex, float duration) {
                finalOrientIndex = destinationIndex;
                currTimer = new Timer(Timer.Type.SINGLE, duration);
            }


            Quaternionf getOrient(Quaternionf initial, boolean slerp) {
                if (slerp) {
                    return slerp(initial, orients[finalOrientIndex], currTimer.getAlpha());
                } else {
                    return lerp(initial, orients[finalOrientIndex], currTimer.getAlpha());
                }
            }

            int getFinalIndex() {
                return finalOrientIndex;
            }
        }


        void updateTime() {
            if (isAnimating) {
                boolean isFinished = anim.updateTime();
                if (isFinished) {
                    isAnimating = false;
                    currOrientIndex = anim.getFinalIndex();
                }
            }
        }

        void animateToOrient(int destinationIndex) {
            if (currOrientIndex == destinationIndex) return;
            anim.startAnimation(destinationIndex, 1.0f);
            isAnimating = true;
        }


        boolean toggleSlerp() {
            slerp = !slerp;
            return slerp;
        }

        Quaternionf getOrient() {
            if (isAnimating) {
                return anim.getOrient(orients[currOrientIndex], slerp);
            } else {
                return orients[currOrientIndex];
            }
        }

        boolean isAnimating() {
            return isAnimating;
        }
    }


    private void applyOrientation(int orientationIndex) {
        if (!orient.isAnimating()) {
            orient.animateToOrient(orientationIndex);
        }
    }


    private Quaternionf slerp(Quaternionf v0, Quaternionf v1, float alpha) {
        final float DOT_THRESHOLD = 0.9995f;
        float dot = v0.dot(v1);
        if (dot > DOT_THRESHOLD) return lerp(v0, v1, alpha);

        dot = Math.min(Math.max(dot, -1.0f), 1.0f);
        float theta_0 = (float) Math.acos(dot);
        float theta = theta_0 * alpha;

        Vector4f p = vectorize(v0).mul(dot).negate();
        Vector4f v2 = vectorize(v1).add(p).normalize();
        Vector4f a = vectorize(v0).mul((float) Math.cos(theta));
        Vector4f b = new Vector4f(v2).mul((float) Math.sin(theta));
        Vector4f res = a.add(b);
        return new Quaternionf(res.x, res.y, res.z, res.w);
    }

    private Quaternionf lerp(Quaternionf v0, Quaternionf v1, float alpha) {
        Vector4f start = vectorize(v0);
        Vector4f end = vectorize(v1);
        Vector4f interp = new Vector4f(start).lerp(end, alpha);

        System.out.printf("alpha: %f, (%f, %f, %f, %f)\n", alpha, interp.w, interp.x, interp.y, interp.z);

        interp.normalize();
        return new Quaternionf(interp.x, interp.y, interp.z, interp.w);
    }

    private Vector4f vectorize(Quaternionf theQuat) {
        Vector4f vec = new Vector4f();
        vec.x = theQuat.x;
        vec.y = theQuat.y;
        vec.z = theQuat.z;
        vec.w = theQuat.w;
        return vec;
    }
} public class QuaternionYPR extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut08/data/";
        new QuaternionYPR().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        ship = new Mesh("Ship.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        rightMultiply = !rightMultiply;
                        System.out.printf(rightMultiply ? "Right-multiply\n" : "Left-multiply\n");
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf currMatrix = new MatrixStackf();
        currMatrix.translate(0.0f, 0.0f, -200.0f);
        currMatrix.mul(orientation.get(new Matrix4f()));

        glUseProgram(theProgram);

        currMatrix.scale(3.0f, 3.0f, 3.0f);
        currMatrix.rotateX((float) Math.toRadians(-90.0f));

        // Set the base color for this object.
        glUniform4f(baseColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
        glUniformMatrix4fv(modelToCameraMatrixUnif, false, currMatrix.get(mat4Buffer));

        ship.render("tint");

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        cameraToClipMatrix.m00(frustumScale * (h / (float) w));
        cameraToClipMatrix.m11(frustumScale);

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float SMALL_ANGLE_INCREMENT = 9.0f;
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            offsetOrientation(new Vector3f(1.0f, 0.0f, 0.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            offsetOrientation(new Vector3f(1.0f, 0.0f, 0.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_A)) {
            offsetOrientation(new Vector3f(0.0f, 0.0f, 1.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_D)) {
            offsetOrientation(new Vector3f(0.0f, 0.0f, 1.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_Q)) {
            offsetOrientation(new Vector3f(0.0f, 1.0f, 0.0f), SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_E)) {
            offsetOrientation(new Vector3f(0.0f, 1.0f, 0.0f), -SMALL_ANGLE_INCREMENT * lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private int theProgram;

    private int modelToCameraMatrixUnif;
    private int cameraToClipMatrixUnif;
    private int baseColorUnif;

    private Matrix4f cameraToClipMatrix = new Matrix4f();

    private final float frustumScale = calcFrustumScale(20.0f);


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "PosColorLocalTransform.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "ColorMultUniform.frag"));
        theProgram = Framework.createProgram(shaderList);

        modelToCameraMatrixUnif = glGetUniformLocation(theProgram, "modelToCameraMatrix");
        cameraToClipMatrixUnif = glGetUniformLocation(theProgram, "cameraToClipMatrix");
        baseColorUnif = glGetUniformLocation(theProgram, "baseColor");

        float zNear = 1.0f;
        float zFar = 600.0f;
        cameraToClipMatrix.m00(frustumScale);
        cameraToClipMatrix.m11(frustumScale);
        cameraToClipMatrix.m22((zFar + zNear) / (zNear - zFar));
        cameraToClipMatrix.m23(-1.0f);
        cameraToClipMatrix.m32((2 * zFar * zNear) / (zNear - zFar));

        glUseProgram(theProgram);
        glUniformMatrix4fv(cameraToClipMatrixUnif, false, cameraToClipMatrix.get(mat4Buffer));
        glUseProgram(0);
    }


    private float calcFrustumScale(float fovDeg) {
        float fovRad = (float) Math.toRadians(fovDeg);
        return (float) (1.0f / Math.tan(fovRad / 2.0f));
    }

    ////////////////////////////////
    private Mesh ship;

    private Quaternionf orientation = new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f);

    private boolean rightMultiply = true;


    private void offsetOrientation(Vector3f axis, float angDeg) {
        float angRad = (float) Math.toRadians(angDeg);

        axis.normalize();
        axis.mul((float) Math.sin(angRad / 2.0f));

        float scalar = (float) Math.cos(angRad / 2.0f);
        Quaternionf offset = new Quaternionf(axis.x, axis.y, axis.z, scalar);

        if (rightMultiply) {
            orientation.mul(offset);
        } else {
            orientation = offset.mul(orientation);
        }

        orientation.normalize();
    }
} public class AmbientLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut09/data/";
        new AmbientLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_T:
                        showAmbient = !showAmbient;
                        if (showAmbient) {
                            System.out.printf("Ambient Lighting On.\n");
                        } else {
                            System.out.printf("Ambient Lighting Off.\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        Vector4f lightDirCameraSpace = modelMatrix.transform(new Vector4f(lightDirection));

        ProgramData whiteDiffuse = showAmbient ? whiteAmbDiffuseColor : whiteDiffuseColor;
        ProgramData vertexDiffuse = showAmbient ? vertexAmbDiffuseColor : vertexDiffuseColor;

        if (showAmbient) {
            glUseProgram(whiteDiffuse.theProgram);
            glUniform4f(whiteDiffuse.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
            glUniform4f(whiteDiffuse.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
            glUseProgram(vertexDiffuse.theProgram);
            glUniform4f(vertexDiffuse.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
            glUniform4f(vertexDiffuse.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        } else {
            glUseProgram(whiteDiffuse.theProgram);
            glUniform4f(whiteDiffuse.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            glUseProgram(vertexDiffuse.theProgram);
            glUniform4f(vertexDiffuse.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
        }

        glUseProgram(whiteDiffuse.theProgram);
        glUniform3fv(whiteDiffuse.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(vertexDiffuse.theProgram);
        glUniform3fv(vertexDiffuse.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                glUseProgram(whiteDiffuse.theProgram);
                glUniformMatrix4fv(whiteDiffuse.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                glUniformMatrix3fv(whiteDiffuse.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (drawColoredCyl) {
                    glUseProgram(vertexDiffuse.theProgram);
                    glUniformMatrix4fv(vertexDiffuse.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(vertexDiffuse.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit-color");
                } else {
                    glUseProgram(whiteDiffuse.theProgram);
                    glUniformMatrix4fv(whiteDiffuse.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(whiteDiffuse.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit");
                }
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData whiteDiffuseColor;
    private ProgramData vertexDiffuseColor;
    private ProgramData whiteAmbDiffuseColor;
    private ProgramData vertexAmbDiffuseColor;

    private class ProgramData {
        int theProgram;

        int dirToLightUnif;
        int lightIntensityUnif;
        int ambientIntensityUnif;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }


    private void initializeProgram() {
        whiteDiffuseColor = loadProgram("DirVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexDiffuseColor = loadProgram("DirVertexLighting_PCN.vert", "ColorPassthrough.frag");
        whiteAmbDiffuseColor = loadProgram("DirAmbVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexAmbDiffuseColor = loadProgram("DirAmbVertexLighting_PCN.vert", "ColorPassthrough.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.dirToLightUnif = glGetUniformLocation(data.theProgram, "dirToLight");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;

    private Vector4f lightDirection = new Vector4f(0.866f, 0.5f, 0.0f, 0.0f);

    private boolean drawColoredCyl = true;
    private boolean showAmbient;

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class BasicLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut09/data/";
        new BasicLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        Vector4f lightDirCameraSpace = modelMatrix.transform(new Vector4f(lightDirection));

        glUseProgram(whiteDiffuseColor.theProgram);
        glUniform3fv(whiteDiffuseColor.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(vertexDiffuseColor.theProgram);
        glUniform3fv(vertexDiffuseColor.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                glUseProgram(whiteDiffuseColor.theProgram);
                glUniformMatrix4fv(whiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                glUniformMatrix3fv(whiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                glUniform4f(whiteDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (drawColoredCyl) {
                    glUseProgram(vertexDiffuseColor.theProgram);
                    glUniformMatrix4fv(vertexDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(vertexDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    glUniform4f(vertexDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                    cylinderMesh.render("lit-color");
                } else {
                    glUseProgram(whiteDiffuseColor.theProgram);
                    glUniformMatrix4fv(whiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(whiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    glUniform4f(whiteDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                    cylinderMesh.render("lit");
                }

                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData whiteDiffuseColor;
    private ProgramData vertexDiffuseColor;

    private class ProgramData {
        int theProgram;

        int dirToLightUnif;
        int lightIntensityUnif;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }


    private void initializeProgram() {
        whiteDiffuseColor = loadProgram("DirVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexDiffuseColor = loadProgram("DirVertexLighting_PCN.vert", "ColorPassthrough.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.dirToLightUnif = glGetUniformLocation(data.theProgram, "dirToLight");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;

    private Vector4f lightDirection = new Vector4f(0.866f, 0.5f, 0.0f, 0.0f);

    private boolean drawColoredCyl = true;

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class ScaleAndLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut09/data/";
        new ScaleAndLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializeProgram();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_T:
                        doInvTranspose = !doInvTranspose;
                        if (doInvTranspose) {
                            System.out.printf("Doing Inverse Transpose.\n");
                        } else {
                            System.out.printf("Bad lighting.\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        Vector4f lightDirCameraSpace = modelMatrix.transform(new Vector4f(lightDirection));

        glUseProgram(whiteDiffuseColor.theProgram);
        glUniform3fv(whiteDiffuseColor.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(vertexDiffuseColor.theProgram);
        glUniform3fv(vertexDiffuseColor.dirToLightUnif, lightDirCameraSpace.get(vec4Buffer));
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                glUseProgram(whiteDiffuseColor.theProgram);
                glUniformMatrix4fv(whiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                glUniformMatrix3fv(whiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                glUniform4f(whiteDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) modelMatrix.scale(1.0f, 1.0f, 0.2f);

                glUseProgram(vertexDiffuseColor.theProgram);
                glUniformMatrix4fv(vertexDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                if (doInvTranspose) normMatrix.invert().transpose();
                glUniformMatrix3fv(vertexDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                glUniform4f(vertexDiffuseColor.lightIntensityUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                cylinderMesh.render("lit-color");
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData whiteDiffuseColor;
    private ProgramData vertexDiffuseColor;

    private class ProgramData {
        int theProgram;

        int dirToLightUnif;
        int lightIntensityUnif;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }


    private void initializeProgram() {
        whiteDiffuseColor = loadProgram("DirVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexDiffuseColor = loadProgram("DirVertexLighting_PCN.vert", "ColorPassthrough.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.dirToLightUnif = glGetUniformLocation(data.theProgram, "dirToLight");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;

    private Vector4f lightDirection = new Vector4f(0.866f, 0.5f, 0.0f, 0.0f);

    private boolean doInvTranspose = true;
    private boolean scaleCyl;

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class FragmentAttenuation extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut10/data/";
        new FragmentAttenuation().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        unprojectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, unprojectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, UnprojectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, unprojectionBlockIndex, unprojectionUniformBuffer, 0, UnprojectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_O:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            lightAttenuation *= 1.1f;
                        } else {
                            lightAttenuation *= 1.5f;
                        }

                        System.out.printf("Atten: %f\n", lightAttenuation);
                        break;

                    case GLFW_KEY_U:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            lightAttenuation /= 1.1f;
                        } else {
                            lightAttenuation /= 1.5f;
                        }

                        System.out.printf("Atten: %f\n", lightAttenuation);
                        break;

                    case GLFW_KEY_Y:
                        drawLight = !drawLight;
                        break;

                    case GLFW_KEY_T:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_H:
                        useRSquare = !useRSquare;
                        if (useRSquare) {
                            System.out.printf("Inverse Squared Attenuation\n");
                        } else {
                            System.out.printf("Plain Inverse Attenuation\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();
        final Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        glUseProgram(fragWhiteDiffuseColor.theProgram);
        glUniform4f(fragWhiteDiffuseColor.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(fragWhiteDiffuseColor.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(fragWhiteDiffuseColor.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUniform1f(fragWhiteDiffuseColor.lightAttenuationUnif, lightAttenuation);
        glUniform1i(fragWhiteDiffuseColor.useRSquareUnif, useRSquare ? 1 : 0);

        glUseProgram(fragVertexDiffuseColor.theProgram);
        glUniform4f(fragVertexDiffuseColor.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(fragVertexDiffuseColor.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(fragVertexDiffuseColor.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUniform1f(fragVertexDiffuseColor.lightAttenuationUnif, lightAttenuation);
        glUniform1i(fragVertexDiffuseColor.useRSquareUnif, useRSquare ? 1 : 0);
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                glUseProgram(fragWhiteDiffuseColor.theProgram);
                glUniformMatrix4fv(fragWhiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(fragWhiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) {
                    modelMatrix.scale(1.0f, 1.0f, 0.2f);
                }

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                if (drawColoredCyl) {
                    glUseProgram(fragVertexDiffuseColor.theProgram);
                    glUniformMatrix4fv(fragVertexDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    glUniformMatrix3fv(fragVertexDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit-color");
                } else {
                    glUseProgram(fragWhiteDiffuseColor.theProgram);
                    glUniformMatrix4fv(fragWhiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    glUniformMatrix3fv(fragWhiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit");
                }
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLight) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        UnprojectionBlock unprojData = new UnprojectionBlock();
        unprojData.clipToCameraMatrix = new Matrix4f(persMatrix).invert();
        unprojData.windowSize = new Vector2i(w, h);

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, unprojectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, unprojData.getAndFlip(unprojectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }


        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }

        if (lightAttenuation < 0.1f) {
            lightAttenuation = 0.1f;
        }
    }

    ////////////////////////////////
    private ProgramData fragWhiteDiffuseColor;
    private ProgramData fragVertexDiffuseColor;
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;

        int lightIntensityUnif;
        int ambientIntensityUnif;

        int normalModelToCameraMatrixUnif;
        int cameraSpaceLightPosUnif;
        int lightAttenuationUnif;
        int useRSquareUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        fragWhiteDiffuseColor = loadLitProgram("FragLightAtten_PN.vert", "FragLightAtten.frag");
        fragVertexDiffuseColor = loadLitProgram("FragLightAtten_PCN.vert", "FragLightAtten.frag");
        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.cameraSpaceLightPosUnif = glGetUniformLocation(data.theProgram, "cameraSpaceLightPos");
        data.lightAttenuationUnif = glGetUniformLocation(data.theProgram, "lightAttenuation");
        data.useRSquareUnif = glGetUniformLocation(data.theProgram, "bUseRSquare");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int unprojectionBlock = glGetUniformBlockIndex(data.theProgram, "UnProjection");
        glUniformBlockBinding(data.theProgram, unprojectionBlock, unprojectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private float lightAttenuation = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean drawColoredCyl;
    private boolean drawLight;
    private boolean scaleCyl;
    private boolean useRSquare;


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;
    private final int unprojectionBlockIndex = 1;

    private int projectionUniformBuffer;
    private int unprojectionUniformBuffer;
} public class FragmentPointLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut10/data/";
        new FragmentPointLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_Y:
                        drawLight = !drawLight;
                        break;

                    case GLFW_KEY_T:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_H:
                        useFragmentLighting = !useFragmentLighting;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();
        final Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        ProgramData whiteProgram;
        ProgramData vertColorProgram;

        if (useFragmentLighting) {
            whiteProgram = fragWhiteDiffuseColor;
            vertColorProgram = fragVertexDiffuseColor;
        } else {
            whiteProgram = whiteDiffuseColor;
            vertColorProgram = vertexDiffuseColor;
        }

        glUseProgram(whiteProgram.theProgram);
        glUniform4f(whiteProgram.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(whiteProgram.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUseProgram(vertColorProgram.theProgram);
        glUniform4f(vertColorProgram.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(vertColorProgram.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                glUseProgram(whiteProgram.theProgram);
                glUniformMatrix4fv(whiteProgram.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                Matrix4f invTransform = new Matrix4f(modelMatrix).invert();
                Vector4f lightPosModelSpace = new Matrix4f(invTransform).transform(lightPosCameraSpace);
                glUniform3fv(whiteProgram.modelSpaceLightPosUnif, lightPosModelSpace.get(vec4Buffer));

                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) {
                    modelMatrix.scale(1.0f, 1.0f, 0.2f);
                }

                Matrix4f invTransform = new Matrix4f(modelMatrix).invert();
                Vector4f lightPosModelSpace = new Matrix4f(invTransform).transform(lightPosCameraSpace);

                if (drawColoredCyl) {
                    glUseProgram(vertColorProgram.theProgram);
                    glUniformMatrix4fv(vertColorProgram.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    glUniform3fv(vertColorProgram.modelSpaceLightPosUnif, lightPosModelSpace.get(vec4Buffer));

                    cylinderMesh.render("lit-color");
                } else {
                    glUseProgram(whiteProgram.theProgram);
                    glUniformMatrix4fv(whiteProgram.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    glUniform3fv(whiteProgram.modelSpaceLightPosUnif, lightPosModelSpace.get(vec4Buffer));

                    cylinderMesh.render("lit");
                }
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLight) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }


        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }
    }

    ////////////////////////////////
    private ProgramData whiteDiffuseColor;
    private ProgramData vertexDiffuseColor;
    private ProgramData fragWhiteDiffuseColor;
    private ProgramData fragVertexDiffuseColor;
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelSpaceLightPosUnif;
        int lightIntensityUnif;
        int ambientIntensityUnif;

        int modelToCameraMatrixUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        whiteDiffuseColor = loadLitProgram("ModelPosVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexDiffuseColor = loadLitProgram("ModelPosVertexLighting_PCN.vert", "ColorPassthrough.frag");
        fragWhiteDiffuseColor = loadLitProgram("FragmentLighting_PN.vert", "FragmentLighting.frag");
        fragVertexDiffuseColor = loadLitProgram("FragmentLighting_PCN.vert", "FragmentLighting.frag");
        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.modelSpaceLightPosUnif = glGetUniformLocation(data.theProgram, "modelSpaceLightPos");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean useFragmentLighting = true;
    private boolean drawColoredCyl;
    private boolean drawLight;
    private boolean scaleCyl;


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class VertexPointLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut10/data/";
        new VertexPointLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(0.0f, 1.0f);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_Y:
                        drawLight = !drawLight;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();

        Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        glUseProgram(whiteDiffuseColor.theProgram);
        glUniform3fv(whiteDiffuseColor.lightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUseProgram(vertexDiffuseColor.theProgram);
        glUniform3fv(vertexDiffuseColor.lightPosUnif, lightPosCameraSpace.get(vec4Buffer));

        glUseProgram(whiteDiffuseColor.theProgram);
        glUniform4f(whiteDiffuseColor.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(whiteDiffuseColor.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUseProgram(vertexDiffuseColor.theProgram);
        glUniform4f(vertexDiffuseColor.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(vertexDiffuseColor.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                glUseProgram(whiteDiffuseColor.theProgram);
                glUniformMatrix4fv(whiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                glUniformMatrix3fv(whiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (drawColoredCyl) {
                    glUseProgram(vertexDiffuseColor.theProgram);
                    glUniformMatrix4fv(vertexDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(vertexDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit-color");
                } else {
                    glUseProgram(whiteDiffuseColor.theProgram);
                    glUniformMatrix4fv(whiteDiffuseColor.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                    Matrix3f normMatrix = new Matrix3f(modelMatrix);
                    glUniformMatrix3fv(whiteDiffuseColor.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                    cylinderMesh.render("lit");
                }
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLight) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }


        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }
    }

    ////////////////////////////////
    private ProgramData whiteDiffuseColor;
    private ProgramData vertexDiffuseColor;
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int lightPosUnif;
        int lightIntensityUnif;
        int ambientIntensityUnif;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        whiteDiffuseColor = loadLitProgram("PosVertexLighting_PN.vert", "ColorPassthrough.frag");
        vertexDiffuseColor = loadLitProgram("PosVertexLighting_PCN.vert", "ColorPassthrough.frag");
        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.lightPosUnif = glGetUniformLocation(data.theProgram, "lightPos");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean drawColoredCyl;
    private boolean drawLight;


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class BlinnVsPhongLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut11/data/";
        new BlinnVsPhongLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_O:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            matParams.increment(false);
                        } else {
                            matParams.increment(true);
                        }

                        System.out.printf("Shiny: %f\n", (float) matParams.getSpecularValue());
                        break;

                    case GLFW_KEY_U:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            matParams.decrement(false);
                        } else {
                            matParams.decrement(true);
                        }

                        System.out.printf("Shiny: %f\n", (float) matParams.getSpecularValue());
                        break;

                    case GLFW_KEY_Y:
                        drawLightSource = !drawLightSource;
                        break;

                    case GLFW_KEY_T:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_G:
                        drawDark = !drawDark;
                        break;

                    case GLFW_KEY_H:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            int index;
                            if (lightModel.ordinal() % 2 != 0) {
                                index = lightModel.ordinal() - 1;
                            } else {
                                index = lightModel.ordinal() + 1;
                            }
                            index %= LightingModel.MAX_LIGHTING_MODEL.ordinal();
                            lightModel = LightingModel.values()[index];
                        } else {
                            int index = lightModel.ordinal() + 2;
                            index %= LightingModel.MAX_LIGHTING_MODEL.ordinal();
                            lightModel = LightingModel.values()[index];
                        }

                        System.out.printf("%s\n", lightModelNames[lightModel.ordinal()]);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();
        final Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        ProgramData whiteProg = programs[lightModel.ordinal()].whiteProg;
        ProgramData colorProg = programs[lightModel.ordinal()].colorProg;

        glUseProgram(whiteProg.theProgram);
        glUniform4f(whiteProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(whiteProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(whiteProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        float lightAttenuation = 1.2f;
        glUniform1f(whiteProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(whiteProg.shininessFactorUnif, matParams.getSpecularValue());
        glUniform4fv(whiteProg.baseDiffuseColorUnif, drawDark ? darkColor.get(vec4Buffer) : lightColor.get(vec4Buffer));

        glUseProgram(colorProg.theProgram);
        glUniform4f(colorProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(colorProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(colorProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUniform1f(colorProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(colorProg.shininessFactorUnif, matParams.getSpecularValue());
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                glUseProgram(whiteProg.theProgram);
                glUniformMatrix4fv(whiteProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(whiteProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) {
                    modelMatrix.scale(1.0f, 1.0f, 0.2f);
                }

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                ProgramData prog = drawColoredCyl ? colorProg : whiteProg;
                glUseProgram(prog.theProgram);
                glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(prog.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

                if (drawColoredCyl) {
                    cylinderMesh.render("lit-color");
                } else {
                    cylinderMesh.render("lit");
                }

                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLightSource) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }


        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }
    }

    ////////////////////////////////
    private ProgramPairs[] programs = new ProgramPairs[LightingModel.MAX_LIGHTING_MODEL.ordinal()];
    private ShaderPairs[] shaderFileNames = new ShaderPairs[]{
            new ShaderPairs("PN.vert", "PCN.vert", "PhongLighting.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "PhongOnly.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "BlinnLighting.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "BlinnOnly.frag")
    };
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;

        int lightIntensityUnif;
        int ambientIntensityUnif;

        int normalModelToCameraMatrixUnif;
        int cameraSpaceLightPosUnif;
        int lightAttenuationUnif;
        int shininessFactorUnif;
        int baseDiffuseColorUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }

    private class ProgramPairs {
        ProgramData whiteProg;
        ProgramData colorProg;
    }

    private class ShaderPairs {
        String whiteVertShaderFileName;
        String colorVertShaderFileName;
        String fragmentShaderFileName;

        ShaderPairs(String whiteVertShaderFileName, String colorVertShaderFileName, String fragmentShaderFileName) {
            this.whiteVertShaderFileName = whiteVertShaderFileName;
            this.colorVertShaderFileName = colorVertShaderFileName;
            this.fragmentShaderFileName = fragmentShaderFileName;
        }
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < LightingModel.MAX_LIGHTING_MODEL.ordinal(); progIndex++) {
            programs[progIndex] = new ProgramPairs();
            programs[progIndex].whiteProg = loadLitProgram(shaderFileNames[progIndex].whiteVertShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
            programs[progIndex].colorProg = loadLitProgram(shaderFileNames[progIndex].colorVertShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
        }

        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.cameraSpaceLightPosUnif = glGetUniformLocation(data.theProgram, "cameraSpaceLightPos");
        data.lightAttenuationUnif = glGetUniformLocation(data.theProgram, "lightAttenuation");
        data.shininessFactorUnif = glGetUniformLocation(data.theProgram, "shininessFactor");
        data.baseDiffuseColorUnif = glGetUniformLocation(data.theProgram, "baseDiffuseColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean drawColoredCyl;
    private boolean drawLightSource;
    private boolean scaleCyl;
    private boolean drawDark;

    private final Vector4f darkColor = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
    private final Vector4f lightColor = new Vector4f(1.0f);

    private final String[] lightModelNames = {
            "Phong Specular.",
            "Phong Only.",
            "Blinn Specular.",
            "Blinn Only."
    };


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private LightingModel lightModel = LightingModel.BLINN_SPECULAR;

    private enum LightingModel {
        PHONG_SPECULAR,
        PHONG_ONLY,
        BLINN_SPECULAR,
        BLINN_ONLY,

        MAX_LIGHTING_MODEL
    }

    ////////////////////////////////
    private MaterialParams matParams = new MaterialParams();

    private class MaterialParams {
        float phongExponent;
        float blinnExponent;


        MaterialParams() {
            phongExponent = 4.0f;
            blinnExponent = 4.0f;
        }


        void increment(boolean isLarge) {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (isLarge) {
                        phongExponent += 0.5f;
                    } else {
                        phongExponent += 0.1f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (isLarge) {
                        blinnExponent += 0.5f;
                    } else {
                        blinnExponent += 0.1f;
                    }
                    break;

                default:
                    break;
            }

            clampParam();
        }

        void decrement(boolean isLarge) {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (isLarge) {
                        phongExponent -= 0.5f;
                    } else {
                        phongExponent -= 0.1f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (isLarge) {
                        blinnExponent -= 0.5f;
                    } else {
                        blinnExponent -= 0.1f;
                    }
                    break;

                default:
                    break;
            }

            clampParam();
        }


        float getSpecularValue() {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    return phongExponent;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    return blinnExponent;

                default:
                    return 0.0f;
            }
        }


        private void clampParam() {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (phongExponent <= 0.0f) {
                        phongExponent = 0.0001f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (blinnExponent <= 0.0f) {
                        blinnExponent = 0.0001f;
                    }
                    break;

                default:
                    break;
            }
        }
    }
} public class GaussianSpecularLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut11/data/";
        new GaussianSpecularLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_O:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            matParams.increment(false);
                        } else {
                            matParams.increment(true);
                        }

                        System.out.printf("Shiny: %f\n", (float) matParams.getSpecularValue());
                        break;

                    case GLFW_KEY_U:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            matParams.decrement(false);
                        } else {
                            matParams.decrement(true);
                        }

                        System.out.printf("Shiny: %f\n", (float) matParams.getSpecularValue());
                        break;

                    case GLFW_KEY_Y:
                        drawLightSource = !drawLightSource;
                        break;

                    case GLFW_KEY_T:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_G:
                        drawDark = !drawDark;
                        break;

                    case GLFW_KEY_H:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            int index;
                            if (lightModel.ordinal() % 2 != 0) {
                                index = lightModel.ordinal() - 1;
                            } else {
                                index = lightModel.ordinal() + 1;
                            }
                            index %= LightingModel.MAX_LIGHTING_MODEL.ordinal();
                            lightModel = LightingModel.values()[index];
                        } else {
                            int index = lightModel.ordinal() + 2;
                            index %= LightingModel.MAX_LIGHTING_MODEL.ordinal();
                            lightModel = LightingModel.values()[index];
                        }

                        System.out.printf("%s\n", lightModelNames[lightModel.ordinal()]);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();
        final Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        ProgramData whiteProg = programs[lightModel.ordinal()].whiteProg;
        ProgramData colorProg = programs[lightModel.ordinal()].colorProg;

        glUseProgram(whiteProg.theProgram);
        glUniform4f(whiteProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(whiteProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(whiteProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        float lightAttenuation = 1.2f;
        glUniform1f(whiteProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(whiteProg.shininessFactorUnif, matParams.getSpecularValue());
        glUniform4fv(whiteProg.baseDiffuseColorUnif, drawDark ? darkColor.get(vec4Buffer) : lightColor.get(vec4Buffer));

        glUseProgram(colorProg.theProgram);
        glUniform4f(colorProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(colorProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(colorProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUniform1f(colorProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(colorProg.shininessFactorUnif, matParams.getSpecularValue());
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                glUseProgram(whiteProg.theProgram);
                glUniformMatrix4fv(whiteProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(whiteProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) {
                    modelMatrix.scale(1.0f, 1.0f, 0.2f);
                }

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                ProgramData prog = drawColoredCyl ? colorProg : whiteProg;
                glUseProgram(prog.theProgram);
                glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(prog.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

                if (drawColoredCyl) {
                    cylinderMesh.render("lit-color");
                } else {
                    cylinderMesh.render("lit");
                }

                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLightSource) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }


        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }
    }

    ////////////////////////////////
    private ProgramPairs[] programs = new ProgramPairs[LightingModel.MAX_LIGHTING_MODEL.ordinal()];
    private ShaderPairs[] shaderFileNames = new ShaderPairs[]{
            new ShaderPairs("PN.vert", "PCN.vert", "PhongLighting.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "PhongOnly.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "BlinnLighting.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "BlinnOnly.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "GaussianLighting.frag"),
            new ShaderPairs("PN.vert", "PCN.vert", "GaussianOnly.frag")
    };
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;

        int lightIntensityUnif;
        int ambientIntensityUnif;

        int normalModelToCameraMatrixUnif;
        int cameraSpaceLightPosUnif;
        int lightAttenuationUnif;
        int shininessFactorUnif;
        int baseDiffuseColorUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }

    private class ProgramPairs {
        ProgramData whiteProg;
        ProgramData colorProg;
    }

    private class ShaderPairs {
        String whiteVertShaderFileName;
        String colorVertShaderFileName;
        String fragmentShaderFileName;

        ShaderPairs(String whiteVertShaderFileName, String colorVertShaderFileName, String fragmentShaderFileName) {
            this.whiteVertShaderFileName = whiteVertShaderFileName;
            this.colorVertShaderFileName = colorVertShaderFileName;
            this.fragmentShaderFileName = fragmentShaderFileName;
        }
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < LightingModel.MAX_LIGHTING_MODEL.ordinal(); progIndex++) {
            programs[progIndex] = new ProgramPairs();
            programs[progIndex].whiteProg = loadLitProgram(shaderFileNames[progIndex].whiteVertShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
            programs[progIndex].colorProg = loadLitProgram(shaderFileNames[progIndex].colorVertShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
        }

        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.cameraSpaceLightPosUnif = glGetUniformLocation(data.theProgram, "cameraSpaceLightPos");
        data.lightAttenuationUnif = glGetUniformLocation(data.theProgram, "lightAttenuation");
        data.shininessFactorUnif = glGetUniformLocation(data.theProgram, "shininessFactor");
        data.baseDiffuseColorUnif = glGetUniformLocation(data.theProgram, "baseDiffuseColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean drawColoredCyl;
    private boolean drawLightSource;
    private boolean scaleCyl;
    private boolean drawDark;

    private final Vector4f darkColor = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
    private final Vector4f lightColor = new Vector4f(1.0f);

    private final String[] lightModelNames = {
            "Phong Specular.",
            "Phong Only.",
            "Blinn Specular.",
            "Blinn Only.",
            "Gaussian Specular.",
            "Gaussian Only."
    };


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private LightingModel lightModel = LightingModel.GAUSSIAN_SPECULAR;

    private enum LightingModel {
        PHONG_SPECULAR,
        PHONG_ONLY,
        BLINN_SPECULAR,
        BLINN_ONLY,
        GAUSSIAN_SPECULAR,
        GAUSSIAN_ONLY,

        MAX_LIGHTING_MODEL
    }

    ////////////////////////////////
    private MaterialParams matParams = new MaterialParams();

    private class MaterialParams {
        float phongExponent;
        float blinnExponent;
        float gaussianRoughness;


        MaterialParams() {
            phongExponent = 4.0f;
            blinnExponent = 4.0f;
            gaussianRoughness = 0.5f;
        }


        void increment(boolean isLarge) {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (isLarge) {
                        phongExponent += 0.5f;
                    } else {
                        phongExponent += 0.1f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (isLarge) {
                        blinnExponent += 0.5f;
                    } else {
                        blinnExponent += 0.1f;
                    }
                    break;

                case GAUSSIAN_SPECULAR:
                case GAUSSIAN_ONLY:
                    if (isLarge) {
                        gaussianRoughness += 0.1f;
                    } else {
                        gaussianRoughness += 0.01f;
                    }
                    break;

                default:
                    break;
            }

            clampParam();
        }

        void decrement(boolean isLarge) {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (isLarge) {
                        phongExponent += 0.5f;
                    } else {
                        phongExponent += 0.1f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (isLarge) {
                        blinnExponent += 0.5f;
                    } else {
                        blinnExponent += 0.1f;
                    }
                    break;

                case GAUSSIAN_SPECULAR:
                case GAUSSIAN_ONLY:
                    if (isLarge) {
                        gaussianRoughness -= 0.1f;
                    } else {
                        gaussianRoughness -= 0.01f;
                    }
                    break;

                default:
                    break;
            }

            clampParam();
        }


        float getSpecularValue() {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    return phongExponent;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    return blinnExponent;

                case GAUSSIAN_SPECULAR:
                case GAUSSIAN_ONLY:
                    return gaussianRoughness;

                default:
                    return 0.0f;
            }
        }


        void clampParam() {
            switch (lightModel) {
                case PHONG_SPECULAR:
                case PHONG_ONLY:
                    if (phongExponent <= 0.0f) {
                        phongExponent = 0.0001f;
                    }
                    break;

                case BLINN_SPECULAR:
                case BLINN_ONLY:
                    if (blinnExponent <= 0.0f) {
                        blinnExponent = 0.0001f;
                    }
                    break;

                case GAUSSIAN_SPECULAR:
                case GAUSSIAN_ONLY:
                    gaussianRoughness = Math.max(0.0001f, gaussianRoughness);
                    gaussianRoughness = Math.min(1.0f, gaussianRoughness);
                    break;

                default:
                    break;
            }
        }
    }
} public class PhongLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut11/data/";
        new PhongLighting().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        cylinderMesh = new Mesh("UnitCylinder.xml");
        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        drawColoredCyl = !drawColoredCyl;
                        break;

                    case GLFW_KEY_O:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            shininessFactor += 0.1f;
                        } else {
                            shininessFactor += 0.5f;
                        }

                        System.out.printf("Shiny: %f\n", shininessFactor);
                        break;

                    case GLFW_KEY_U:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            shininessFactor -= 0.1f;
                        } else {
                            shininessFactor -= 0.5f;
                        }

                        System.out.printf("Shiny: %f\n", shininessFactor);
                        break;

                    case GLFW_KEY_Y:
                        drawLightSource = !drawLightSource;
                        break;

                    case GLFW_KEY_T:
                        scaleCyl = !scaleCyl;
                        break;

                    case GLFW_KEY_B:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_G:
                        drawDark = !drawDark;
                        break;

                    case GLFW_KEY_H:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            switch (lightModel) {
                                case DIFFUSE_AND_SPECULAR:
                                    lightModel = LightingModel.PURE_DIFFUSE;
                                    break;

                                case PURE_DIFFUSE:
                                    lightModel = LightingModel.DIFFUSE_AND_SPECULAR;
                                    break;

                                case SPECULAR_ONLY:
                                    lightModel = LightingModel.PURE_DIFFUSE;
                                    break;
                            }
                        } else {
                            int index = lightModel.ordinal() + 1;
                            index %= LightingModel.MAX_LIGHTING_MODEL.ordinal();
                            lightModel = LightingModel.values()[index];
                        }

                        System.out.printf("%s\n", lightModelNames[lightModel.ordinal()]);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Vector4f worldLightPos = calcLightPosition();
        final Vector4f lightPosCameraSpace = modelMatrix.transform(new Vector4f(worldLightPos));

        ProgramData whiteProg = null;
        ProgramData colorProg = null;

        switch (lightModel) {
            case PURE_DIFFUSE:
                whiteProg = whiteNoPhong;
                colorProg = colorNoPhong;
                break;

            case DIFFUSE_AND_SPECULAR:
                whiteProg = whitePhong;
                colorProg = colorPhong;
                break;

            case SPECULAR_ONLY:
                whiteProg = whitePhongOnly;
                colorProg = colorPhongOnly;
                break;
        }

        assert whiteProg != null;
        glUseProgram(whiteProg.theProgram);
        glUniform4f(whiteProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(whiteProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(whiteProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        float lightAttenuation = 1.2f;
        glUniform1f(whiteProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(whiteProg.shininessFactorUnif, shininessFactor);
        glUniform4fv(whiteProg.baseDiffuseColorUnif, drawDark ? darkColor.get(vec4Buffer) : lightColor.get(vec4Buffer));

        assert colorProg != null;
        glUseProgram(colorProg.theProgram);
        glUniform4f(colorProg.lightIntensityUnif, 0.8f, 0.8f, 0.8f, 1.0f);
        glUniform4f(colorProg.ambientIntensityUnif, 0.2f, 0.2f, 0.2f, 1.0f);
        glUniform3fv(colorProg.cameraSpaceLightPosUnif, lightPosCameraSpace.get(vec4Buffer));
        glUniform1f(colorProg.lightAttenuationUnif, lightAttenuation);
        glUniform1f(colorProg.shininessFactorUnif, shininessFactor);
        glUseProgram(0);

        {
            modelMatrix.pushMatrix();

            // Render the ground plane.
            {
                modelMatrix.pushMatrix();

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                glUseProgram(whiteProg.theProgram);
                glUniformMatrix4fv(whiteProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(whiteProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));
                planeMesh.render();
                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the Cylinder
            {
                modelMatrix.pushMatrix();

                modelMatrix.mul(objtPole.calcMatrix());

                if (scaleCyl) {
                    modelMatrix.scale(1.0f, 1.0f, 0.2f);
                }

                Matrix3f normMatrix = new Matrix3f(modelMatrix);
                normMatrix.invert().transpose();

                ProgramData prog = drawColoredCyl ? colorProg : whiteProg;
                glUseProgram(prog.theProgram);
                glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                glUniformMatrix3fv(prog.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

                if (drawColoredCyl) {
                    cylinderMesh.render("lit-color");
                } else {
                    cylinderMesh.render("lit");
                }

                glUseProgram(0);

                modelMatrix.popMatrix();
            }

            // Render the light
            if (drawLightSource) {
                modelMatrix.pushMatrix();

                modelMatrix.translate(worldLightPos.x, worldLightPos.y, worldLightPos.z);
                modelMatrix.scale(0.1f, 0.1f, 0.1f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.8078f, 0.8706f, 0.9922f, 1.0f);
                cubeMesh.render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 5;

        if (isKeyPressed(GLFW_KEY_J)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius -= 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius -= 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_L)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightRadius += 0.05f * lastFrameDuration * scale;
            } else {
                lightRadius += 0.2f * lastFrameDuration * scale;
            }
        }

        if (isKeyPressed(GLFW_KEY_I)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight += 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight += 0.2f * lastFrameDuration * scale;
            }
        } else if (isKeyPressed(GLFW_KEY_K)) {
            if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                lightHeight -= 0.05f * lastFrameDuration * scale;
            } else {
                lightHeight -= 0.2f * lastFrameDuration * scale;
            }
        }

        if (lightRadius < 0.2f) {
            lightRadius = 0.2f;
        }

        if (shininessFactor <= 0.0f) {
            shininessFactor = 0.0001f;
        }
    }

    ////////////////////////////////
    private ProgramData whiteNoPhong;
    private ProgramData colorNoPhong;
    private ProgramData whitePhong;
    private ProgramData colorPhong;
    private ProgramData whitePhongOnly;
    private ProgramData colorPhongOnly;
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;

        int lightIntensityUnif;
        int ambientIntensityUnif;

        int normalModelToCameraMatrixUnif;
        int cameraSpaceLightPosUnif;
        int lightAttenuationUnif;
        int shininessFactorUnif;
        int baseDiffuseColorUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        whiteNoPhong = loadLitProgram("PN.vert", "NoPhong.frag");
        colorNoPhong = loadLitProgram("PCN.vert", "NoPhong.frag");
        whitePhong = loadLitProgram("PN.vert", "PhongLighting.frag");
        colorPhong = loadLitProgram("PCN.vert", "PhongLighting.frag");
        whitePhongOnly = loadLitProgram("PN.vert", "PhongOnly.frag");
        colorPhongOnly = loadLitProgram("PCN.vert", "PhongOnly.frag");
        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.lightIntensityUnif = glGetUniformLocation(data.theProgram, "lightIntensity");
        data.ambientIntensityUnif = glGetUniformLocation(data.theProgram, "ambientIntensity");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");
        data.cameraSpaceLightPosUnif = glGetUniformLocation(data.theProgram, "cameraSpaceLightPos");
        data.lightAttenuationUnif = glGetUniformLocation(data.theProgram, "lightAttenuation");
        data.shininessFactorUnif = glGetUniformLocation(data.theProgram, "shininessFactor");
        data.baseDiffuseColorUnif = glGetUniformLocation(data.theProgram, "baseDiffuseColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh cylinderMesh;
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private float lightHeight = 1.5f;
    private float lightRadius = 1.0f;
    private Timer lightTimer = new Timer(Timer.Type.LOOP, 5.0f);
    private float shininessFactor = 4.0f;

    private boolean drawColoredCyl;
    private boolean drawLightSource;
    private boolean scaleCyl;
    private boolean drawDark;

    private final Vector4f darkColor = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
    private final Vector4f lightColor = new Vector4f(1.0f);

    private final String[] lightModelNames = {
            "Diffuse only.",
            "Specular + diffuse.",
            "Specular only."
    };


    private Vector4f calcLightPosition() {
        float currTimeThroughLoop = lightTimer.getAlpha();

        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        lightPos.z = (float) (Math.sin(currTimeThroughLoop * (3.14159f * 2.0f)) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            5.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 20.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private LightingModel lightModel = LightingModel.DIFFUSE_AND_SPECULAR;

    private enum LightingModel {
        PURE_DIFFUSE,
        DIFFUSE_AND_SPECULAR,
        SPECULAR_ONLY,

        MAX_LIGHTING_MODEL
    }
} public class GammaCorrection extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut12/data/";
        new GammaCorrection().start(700, 700);
    }


    @Override
    protected void init() {
        initializePrograms();

        scene = new Scene() {
            @Override
            ProgramData getProgram(LightingProgramTypes lightingProgramType) {
                return programs[lightingProgramType.ordinal()];
            }
        };

        setupHDRLighting();

        lights.createTimer("tetra", Timer.Type.LOOP, 2.5f);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightManager.LightBlockGamma.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightManager.LightBlockGamma.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        lights.togglePause(timerMode);
                        break;

                    case GLFW_KEY_MINUS:
                        lights.rewindTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lights.fastForwardTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_1:
                        timerMode = LightManager.TimerTypes.ALL;
                        System.out.printf("All\n");
                        break;

                    case GLFW_KEY_2:
                        timerMode = LightManager.TimerTypes.SUN;
                        System.out.printf("Sun\n");
                        break;

                    case GLFW_KEY_3:
                        timerMode = LightManager.TimerTypes.LIGHTS;
                        System.out.printf("Lights\n");
                        break;

                    case GLFW_KEY_L:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            setupGammaLighting();
                        } else {
                            setupHDRLighting();
                        }
                        break;

                    case GLFW_KEY_K:
                        isGammaCorrect = !isGammaCorrect;
                        if (isGammaCorrect) {
                            System.out.printf("Gamma on!\n");
                        } else {
                            System.out.printf("Gamma off!\n");
                        }
                        break;

                    case GLFW_KEY_Y:
                        gammaValue += 0.1f;
                        System.out.printf("Gamma: %f\n", gammaValue);
                        break;

                    case GLFW_KEY_H:
                        gammaValue -= 0.1f;
                        if (gammaValue < 1.0f) {
                            gammaValue = 1.0f;
                        }
                        System.out.printf("Gamma: %f\n", gammaValue);
                        break;

                    case GLFW_KEY_SPACE:
                        float sunAlpha = lights.getSunTime();
                        float sunTimeHours = sunAlpha * 24.0f + 12.0f;
                        sunTimeHours = sunTimeHours > 24.0f ? sunTimeHours - 24.0f : sunTimeHours;
                        int sunHours = (int) sunTimeHours;
                        float sunTimeMinutes = (sunTimeHours - sunHours) * 60.0f;
                        int sunMinutes = (int) sunTimeMinutes;
                        System.out.printf("%02d:%02d\n", sunHours, sunMinutes);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lights.updateTime(elapsedTime);

        float gamma = isGammaCorrect ? gammaValue : 1.0f;

        Vector4f bkg = gammaCorrect(lights.getBackgroundColor(), gamma);

        glClearColor(bkg.x, bkg.y, bkg.z, bkg.w);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Matrix4f worldToCamMat = modelMatrix;
        LightManager.LightBlockGamma lightData = lights.getLightInformationGamma(worldToCamMat);
        lightData.gamma = gamma;

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            modelMatrix.pushMatrix();

            scene.draw(modelMatrix, materialBlockIndex, lights.getTimerValue("tetra"));

            modelMatrix.popMatrix();
        }

        {
            modelMatrix.pushMatrix();

            // Render the sun
            {
                modelMatrix.pushMatrix();

                Vector4f temp = lights.getSunlightDirection();
                Vector3f sunlightDir = new Vector3f(temp.x, temp.y, temp.z);
                modelMatrix.translate(sunlightDir.mul(500.0f));
                modelMatrix.scale(30.0f, 30.0f, 30.0f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                Vector4f lightColor = lights.getSunlightIntensity();
                glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                scene.getSphereMesh().render("flat");

                modelMatrix.popMatrix();
            }

            // Render the lights
            {
                for (int light = 0; light < lights.getNumberOfPointLights(); light++) {
                    modelMatrix.pushMatrix();

                    modelMatrix.translate(lights.getWorldLightPosition(light));

                    glUseProgram(unlit.theProgram);
                    glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    Vector4f lightColor = lights.getPointLightIntensity(light);
                    glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                    scene.getCubeMesh().render("flat");

                    modelMatrix.popMatrix();
                }
            }

            if (drawCameraPos) {
                modelMatrix.pushMatrix();

                modelMatrix.identity();
                modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);

                glDisable(GL_DEPTH_TEST);
                glDepthMask(false);
                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
                scene.getCubeMesh().render("flat");
                glDepthMask(true);
                glEnable(GL_DEPTH_TEST);
                glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                scene.getCubeMesh().render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 20;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private final int materialBlockIndex = 0;
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    private Scene.ProgramData[] programs = new Scene.ProgramData[Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal()];
    private Shaders[] shaderFileNames = new Shaders[]{
            new Shaders("PCN.vert", "DiffuseSpecularGamma.frag"),
            new Shaders("PCN.vert", "DiffuseOnlyGamma.frag"),

            new Shaders("PN.vert", "DiffuseSpecularMtlGamma.frag"),
            new Shaders("PN.vert", "DiffuseOnlyMtlGamma.frag")
    };
    private UnlitProgData unlit;

    private class Shaders {
        String vertexShaderFileName;
        String fragmentShaderFileName;

        Shaders(String vertexShaderFileName, String fragmentShaderFileName) {
            this.vertexShaderFileName = vertexShaderFileName;
            this.fragmentShaderFileName = fragmentShaderFileName;
        }
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal(); progIndex++) {
            programs[progIndex] = new Scene.ProgramData();
            programs[progIndex] = loadLitProgram(shaderFileNames[progIndex].vertexShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
        }

        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private Scene.ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        Scene.ProgramData data = new Scene.ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        if (materialBlock != GL_INVALID_INDEX) {  // Can be optimized out.
            glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        }
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Scene scene;

    private LightManager lights = new LightManager();
    private final Vector4f skyDaylightColor = new Vector4f(0.65f, 0.65f, 1.0f, 1.0f);

    private LightManager.TimerTypes timerMode = LightManager.TimerTypes.ALL;

    private boolean drawCameraPos;
    private boolean isGammaCorrect;

    private float gammaValue = 2.2f;


    private void setupHDRLighting() {
        LightManager.SunlightValueHDR values[] = {
                new LightManager.SunlightValueHDR(
                        0.0f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        4.5f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        6.5f / 24.0f,
                        new Vector4f(0.225f, 0.075f, 0.075f, 1.0f),
                        new Vector4f(0.45f, 0.15f, 0.15f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        1.5f),
                new LightManager.SunlightValueHDR(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        1.0f),
                new LightManager.SunlightValueHDR(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        1.0f),
                new LightManager.SunlightValueHDR(
                        19.5f / 24.0f,
                        new Vector4f(0.225f, 0.075f, 0.075f, 1.0f),
                        new Vector4f(0.45f, 0.15f, 0.15f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        1.5f),
                new LightManager.SunlightValueHDR(
                        20.5f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f),
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.6f, 0.6f, 0.6f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.7f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.7f, 0.0f, 0.0f, 1.0f));
    }

    private void setupGammaLighting() {
        Vector4f sunlight = new Vector4f(6.5f, 6.5f, 6.5f, 1.0f);
        Vector4f brightAmbient = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);

        LightManager.SunlightValueHDR values[] = {
                new LightManager.SunlightValueHDR(
                        0.0f / 24.0f,
                        brightAmbient,
                        sunlight,
                        new Vector4f(0.65f, 0.65f, 1.0f, 1.0f),
                        10.0f),
                new LightManager.SunlightValueHDR(
                        4.5f / 24.0f,
                        brightAmbient,
                        sunlight,
                        new Vector4f(skyDaylightColor),
                        10.0f),
                new LightManager.SunlightValueHDR(
                        6.5f / 24.0f,
                        new Vector4f(0.01f, 0.025f, 0.025f, 1.0f),
                        new Vector4f(2.5f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        5.0f),
                new LightManager.SunlightValueHDR(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        19.5f / 24.0f,
                        new Vector4f(0.01f, 0.025f, 0.025f, 1.0f),
                        new Vector4f(2.5f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        5.0f),
                new LightManager.SunlightValueHDR(
                        20.5f / 24.0f,
                        brightAmbient,
                        sunlight,
                        new Vector4f(skyDaylightColor),
                        10.0f)
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.6f, 0.6f, 0.6f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.7f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.7f, 0.0f, 0.0f, 1.0f));
    }


    private Vector4f gammaCorrect(Vector4f input, float gamma) {
        Vector4f inputCorrected = new Vector4f();
        inputCorrected.x = (float) Math.pow(input.x, 1.0f / gamma);
        inputCorrected.y = (float) Math.pow(input.y, 1.0f / gamma);
        inputCorrected.z = (float) Math.pow(input.z, 1.0f / gamma);
        inputCorrected.w = input.w;
        return inputCorrected;
    }

    ////////////////////////////////
    // View setup.
    private MousePoles.ViewData initialViewData = new MousePoles.ViewData(
            new Vector3f(-59.5f, 44.0f, 95.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            50.0f,
            0.0f
    );

    private MousePoles.ViewScale viewScale = new MousePoles.ViewScale(
            3.0f, 80.0f,
            4.0f, 1.0f,
            5.0f, 1.0f,
            90.0f / 250.0f
    );


    private MousePoles.ViewPole viewPole = new MousePoles.ViewPole(initialViewData, viewScale, MousePoles.MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class HDRLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut12/data/";
        new HDRLighting().start(700, 700);
    }


    @Override
    protected void init() {
        initializePrograms();

        scene = new Scene() {
            @Override
            ProgramData getProgram(LightingProgramTypes lightingProgramType) {
                return programs[lightingProgramType.ordinal()];
            }
        };

        setupDaytimeLighting();

        lights.createTimer("tetra", Timer.Type.LOOP, 2.5f);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightManager.LightBlockHDR.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightManager.LightBlockHDR.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        lights.togglePause(timerMode);
                        break;

                    case GLFW_KEY_MINUS:
                        lights.rewindTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lights.fastForwardTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_1:
                        timerMode = LightManager.TimerTypes.ALL;
                        System.out.printf("All\n");
                        break;

                    case GLFW_KEY_2:
                        timerMode = LightManager.TimerTypes.SUN;
                        System.out.printf("Sun\n");
                        break;

                    case GLFW_KEY_3:
                        timerMode = LightManager.TimerTypes.LIGHTS;
                        System.out.printf("Lights\n");
                        break;

                    case GLFW_KEY_L:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            setupNighttimeLighting();
                        } else {
                            setupDaytimeLighting();
                        }
                        break;

                    case GLFW_KEY_K:
                        setupHDRLighting();
                        break;

                    case GLFW_KEY_SPACE:
                        float sunAlpha = lights.getSunTime();
                        float sunTimeHours = sunAlpha * 24.0f + 12.0f;
                        sunTimeHours = sunTimeHours > 24.0f ? sunTimeHours - 24.0f : sunTimeHours;
                        int sunHours = (int) sunTimeHours;
                        float sunTimeMinutes = (sunTimeHours - sunHours) * 60.0f;
                        int sunMinutes = (int) sunTimeMinutes;
                        System.out.printf("%02d:%02d\n", sunHours, sunMinutes);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lights.updateTime(elapsedTime);

        Vector4f bkg = lights.getBackgroundColor();

        glClearColor(bkg.x, bkg.y, bkg.z, bkg.w);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Matrix4f worldToCamMat = modelMatrix;
        LightManager.LightBlockHDR lightData = lights.getLightInformationHDR(worldToCamMat);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            modelMatrix.pushMatrix();

            scene.draw(modelMatrix, materialBlockIndex, lights.getTimerValue("tetra"));

            modelMatrix.popMatrix();
        }

        {
            modelMatrix.pushMatrix();

            // Render the sun
            {
                modelMatrix.pushMatrix();

                Vector4f temp = lights.getSunlightDirection();
                Vector3f sunlightDir = new Vector3f(temp.x, temp.y, temp.z);
                modelMatrix.translate(sunlightDir.mul(500.0f));
                modelMatrix.scale(30.0f, 30.0f, 30.0f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                Vector4f lightColor = lights.getSunlightIntensity();
                glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                scene.getSphereMesh().render("flat");

                modelMatrix.popMatrix();
            }

            // Render the lights
            {
                for (int light = 0; light < lights.getNumberOfPointLights(); light++) {
                    modelMatrix.pushMatrix();

                    modelMatrix.translate(lights.getWorldLightPosition(light));

                    glUseProgram(unlit.theProgram);
                    glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    Vector4f lightColor = lights.getPointLightIntensity(light);
                    glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                    scene.getCubeMesh().render("flat");

                    modelMatrix.popMatrix();
                }
            }

            if (drawCameraPos) {
                modelMatrix.pushMatrix();

                modelMatrix.identity();
                modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);

                glDisable(GL_DEPTH_TEST);
                glDepthMask(false);
                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
                scene.getCubeMesh().render("flat");
                glDepthMask(true);
                glEnable(GL_DEPTH_TEST);
                glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                scene.getCubeMesh().render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 20;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private final int materialBlockIndex = 0;
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    private Scene.ProgramData[] programs = new Scene.ProgramData[Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal()];
    private Shaders[] shaderFileNames = new Shaders[]{
            new Shaders("PCN.vert", "DiffuseSpecularHDR.frag"),
            new Shaders("PCN.vert", "DiffuseOnlyHDR.frag"),

            new Shaders("PN.vert", "DiffuseSpecularMtlHDR.frag"),
            new Shaders("PN.vert", "DiffuseOnlyMtlHDR.frag")
    };
    private UnlitProgData unlit;

    private class Shaders {
        String vertexShaderFileName;
        String fragmentShaderFileName;

        Shaders(String vertexShaderFileName, String fragmentShaderFileName) {
            this.vertexShaderFileName = vertexShaderFileName;
            this.fragmentShaderFileName = fragmentShaderFileName;
        }
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal(); progIndex++) {
            programs[progIndex] = new Scene.ProgramData();
            programs[progIndex] = loadLitProgram(shaderFileNames[progIndex].vertexShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
        }

        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private Scene.ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        Scene.ProgramData data = new Scene.ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        if (materialBlock != GL_INVALID_INDEX) {  // Can be optimized out.
            glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        }
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Scene scene;

    private LightManager lights = new LightManager();
    private final Vector4f skyDaylightColor = new Vector4f(0.65f, 0.65f, 1.0f, 1.0f);

    private LightManager.TimerTypes timerMode = LightManager.TimerTypes.ALL;

    private boolean drawCameraPos;


    private void setupDaytimeLighting() {
        LightManager.SunlightValue values[] = {
                new LightManager.SunlightValue(
                        0.0f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        4.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        6.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.10f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        19.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.1f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        20.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor))
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.2f, 0.2f, 0.2f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.3f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.3f, 0.0f, 0.0f, 1.0f));
    }

    private void setupNighttimeLighting() {
        LightManager.SunlightValue values[] = {
                new LightManager.SunlightValue(
                        0.0f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        4.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        6.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.10f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        19.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.1f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        20.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor))
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.6f, 0.6f, 0.6f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.7f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.7f, 0.0f, 0.0f, 1.0f));
    }

    private void setupHDRLighting() {
        LightManager.SunlightValueHDR values[] = {
                new LightManager.SunlightValueHDR(
                        0.0f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        4.5f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f),
                new LightManager.SunlightValueHDR(
                        6.5f / 24.0f,
                        new Vector4f(0.225f, 0.075f, 0.075f, 1.0f),
                        new Vector4f(0.45f, 0.15f, 0.15f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        1.5f),
                new LightManager.SunlightValueHDR(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        1.0f),
                new LightManager.SunlightValueHDR(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        1.0f),
                new LightManager.SunlightValueHDR(
                        19.5f / 24.0f,
                        new Vector4f(0.225f, 0.075f, 0.075f, 1.0f),
                        new Vector4f(0.45f, 0.15f, 0.15f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f),
                        1.5f),
                new LightManager.SunlightValueHDR(
                        20.5f / 24.0f,
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(1.8f, 1.8f, 1.8f, 1.0f),
                        new Vector4f(skyDaylightColor),
                        3.0f)
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.6f, 0.6f, 0.6f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.7f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.7f, 0.0f, 0.0f, 1.0f));
    }

    ////////////////////////////////
    // View setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(-59.5f, 44.0f, 95.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            50.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 80.0f,
            4.0f, 1.0f,
            5.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class SceneLighting extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut12/data/";
        new SceneLighting().start(700, 700);
    }


    @Override
    protected void init() {
        initializePrograms();

        scene = new Scene() {
            @Override
            ProgramData getProgram(LightingProgramTypes lightingProgramType) {
                return programs[lightingProgramType.ordinal()];
            }
        };

        setupDaytimeLighting();

        lights.createTimer("tetra", Timer.Type.LOOP, 2.5f);

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        lights.togglePause(timerMode);
                        break;

                    case GLFW_KEY_MINUS:
                        lights.rewindTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lights.fastForwardTime(timerMode, 1.0f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_1:
                        timerMode = LightManager.TimerTypes.ALL;
                        System.out.printf("All\n");
                        break;

                    case GLFW_KEY_2:
                        timerMode = LightManager.TimerTypes.SUN;
                        System.out.printf("Sun\n");
                        break;

                    case GLFW_KEY_3:
                        timerMode = LightManager.TimerTypes.LIGHTS;
                        System.out.printf("Lights\n");
                        break;

                    case GLFW_KEY_L:
                        if (isKeyPressed(GLFW_KEY_LEFT_SHIFT) || isKeyPressed(GLFW_KEY_RIGHT_SHIFT)) {
                            setupNighttimeLighting();
                        } else {
                            setupDaytimeLighting();
                        }
                        break;

                    case GLFW_KEY_SPACE:
                        float sunAlpha = lights.getSunTime();
                        float sunTimeHours = sunAlpha * 24.0f + 12.0f;
                        sunTimeHours = sunTimeHours > 24.0f ? sunTimeHours - 24.0f : sunTimeHours;
                        int sunHours = (int) sunTimeHours;
                        float sunTimeMinutes = (sunTimeHours - sunHours) * 60.0f;
                        int sunMinutes = (int) sunTimeMinutes;
                        System.out.printf("%02d:%02d\n", sunHours, sunMinutes);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lights.updateTime(elapsedTime);

        Vector4f bkg = lights.getBackgroundColor();
        glClearColor(bkg.x, bkg.y, bkg.z, bkg.w);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        final Matrix4f worldToCamMat = modelMatrix;
        LightBlock lightData = lights.getLightInformation(worldToCamMat);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            modelMatrix.pushMatrix();

            scene.draw(modelMatrix, materialBlockIndex, lights.getTimerValue("tetra"));

            modelMatrix.popMatrix();
        }

        {
            modelMatrix.pushMatrix();

            // Render the sun
            {
                modelMatrix.pushMatrix();

                Vector4f temp = lights.getSunlightDirection();
                Vector3f sunlightDir = new Vector3f(temp.x, temp.y, temp.z);
                modelMatrix.translate(sunlightDir.mul(500.0f));
                modelMatrix.scale(30.0f, 30.0f, 30.0f);

                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                Vector4f lightColor = lights.getSunlightIntensity();
                glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                scene.getSphereMesh().render("flat");

                modelMatrix.popMatrix();
            }

            // Render the lights
            {
                for (int light = 0; light < lights.getNumberOfPointLights(); light++) {
                    modelMatrix.pushMatrix();

                    modelMatrix.translate(lights.getWorldLightPosition(light));

                    glUseProgram(unlit.theProgram);
                    glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

                    Vector4f lightColor = lights.getPointLightIntensity(light);
                    glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
                    scene.getCubeMesh().render("flat");

                    modelMatrix.popMatrix();
                }
            }

            if (drawCameraPos) {
                modelMatrix.pushMatrix();

                modelMatrix.identity();
                modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);

                glDisable(GL_DEPTH_TEST);
                glDepthMask(false);
                glUseProgram(unlit.theProgram);
                glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
                glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
                scene.getCubeMesh().render("flat");
                glDepthMask(true);
                glEnable(GL_DEPTH_TEST);
                glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
                scene.getCubeMesh().render("flat");

                modelMatrix.popMatrix();
            }

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 20;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT)
                    || isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private final int materialBlockIndex = 0;
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    private Scene.ProgramData[] programs = new Scene.ProgramData[Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal()];
    private Shaders[] shaderFileNames = new Shaders[]{
            new Shaders("PCN.vert", "DiffuseSpecular.frag"),
            new Shaders("PCN.vert", "DiffuseOnly.frag"),

            new Shaders("PN.vert", "DiffuseSpecularMtl.frag"),
            new Shaders("PN.vert", "DiffuseOnlyMtl.frag")
    };
    private UnlitProgData unlit;

    private class Shaders {
        String vertexShaderFileName;
        String fragmentShaderFileName;

        Shaders(String vertexShaderFileName, String fragmentShaderFileName) {
            this.vertexShaderFileName = vertexShaderFileName;
            this.fragmentShaderFileName = fragmentShaderFileName;
        }
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < Scene.LightingProgramTypes.MAX_LIGHTING_PROGRAM_TYPES.ordinal(); progIndex++) {
            programs[progIndex] = new Scene.ProgramData();
            programs[progIndex] = loadLitProgram(shaderFileNames[progIndex].vertexShaderFileName,
                    shaderFileNames[progIndex].fragmentShaderFileName);
        }

        unlit = loadUnlitProgram("PosTransform.vert", "UniformColor.frag");
    }

    private Scene.ProgramData loadLitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        Scene.ProgramData data = new Scene.ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        if (materialBlock != GL_INVALID_INDEX) {  // Can be optimized out.
            glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        }
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Scene scene;

    private LightManager lights = new LightManager();
    private final Vector4f skyDaylightColor = new Vector4f(0.65f, 0.65f, 1.0f, 1.0f);

    private LightManager.TimerTypes timerMode = LightManager.TimerTypes.ALL;

    private boolean drawCameraPos;


    private void setupDaytimeLighting() {
        LightManager.SunlightValue values[] = {
                new LightManager.SunlightValue(
                        0.0f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        4.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        6.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.10f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        19.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.1f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        20.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor))
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.2f, 0.2f, 0.2f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.3f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.3f, 0.0f, 0.0f, 1.0f));
    }

    private void setupNighttimeLighting() {
        LightManager.SunlightValue values[] = {
                new LightManager.SunlightValue(
                        0.0f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        4.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor)),
                new LightManager.SunlightValue(
                        6.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.10f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        8.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        18.0f / 24.0f,
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f),
                        new Vector4f(0.0f, 0.0f, 0.0f, 1.0f)),
                new LightManager.SunlightValue(
                        19.5f / 24.0f,
                        new Vector4f(0.15f, 0.05f, 0.05f, 1.0f),
                        new Vector4f(0.3f, 0.1f, 0.1f, 1.0f),
                        new Vector4f(0.5f, 0.1f, 0.1f, 1.0f)),
                new LightManager.SunlightValue(
                        20.5f / 24.0f,
                        new Vector4f(0.2f, 0.2f, 0.2f, 1.0f),
                        new Vector4f(0.6f, 0.6f, 0.6f, 1.0f),
                        new Vector4f(skyDaylightColor))
        };

        lights.setSunlightValues(values, 7);

        lights.setPointLightIntensity(0, new Vector4f(0.6f, 0.6f, 0.6f, 1.0f));
        lights.setPointLightIntensity(1, new Vector4f(0.0f, 0.0f, 0.7f, 1.0f));
        lights.setPointLightIntensity(2, new Vector4f(0.7f, 0.0f, 0.0f, 1.0f));
    }

    ////////////////////////////////
    // View setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(-59.5f, 44.0f, 95.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            50.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 80.0f,
            4.0f, 1.0f,
            5.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;
} public class BasicImpostor extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut13/data/";
        new BasicImpostor().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        planeMesh = new Mesh("LargePlane.xml");
        sphereMesh = new Mesh("UnitSphere.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        // Empty Vertex Array Object.
        imposterVAO = glGenVertexArrays();
        glBindVertexArray(imposterVAO);

        createMaterials();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        sphereTimer.togglePause();
                        break;

                    case GLFW_KEY_MINUS:
                        sphereTimer.rewind(0.5f);
                        break;

                    case GLFW_KEY_EQUAL:
                        sphereTimer.fastForward(0.5f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        drawLights = !drawLights;
                        break;

                    case GLFW_KEY_1:
                        drawImposter[0] = !drawImposter[0];
                        break;

                    case GLFW_KEY_2:
                        drawImposter[1] = !drawImposter[1];
                        break;

                    case GLFW_KEY_3:
                        drawImposter[2] = !drawImposter[2];
                        break;

                    case GLFW_KEY_4:
                        drawImposter[3] = !drawImposter[3];
                        break;

                    case GLFW_KEY_L:
                        currImpostor = Impostors.BASIC;
                        break;

                    case GLFW_KEY_J:
                        currImpostor = Impostors.PERSPECTIVE;
                        break;

                    case GLFW_KEY_H:
                        currImpostor = Impostors.DEPTH;
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        sphereTimer.update(elapsedTime);

        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());
        final Matrix4f worldToCamMat = modelMatrix;

        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        float halfLightDistance = 25.0f;
        lightData.lightAttenuation = 1.0f / (halfLightDistance * halfLightDistance);

        lightData.lights[0] = new PerLight();
        lightData.lights[0].cameraSpaceLightPos = new Matrix4f(worldToCamMat).transform(new Vector4f(0.707f, 0.707f, 0.0f, 0.0f));
        lightData.lights[0].lightIntensity = new Vector4f(0.6f, 0.6f, 0.6f, 1.0f);

        lightData.lights[1] = new PerLight();
        lightData.lights[1].cameraSpaceLightPos = new Matrix4f(worldToCamMat).transform(calcLightPosition());
        lightData.lights[1].lightIntensity = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer,
                    MaterialNames.TERRAIN.ordinal() * materialBlockOffset, MaterialBlock.BYTES);

            Matrix3f normMatrix = new Matrix3f(modelMatrix);
            normMatrix.invert().transpose();

            glUseProgram(litMeshProg.theProgram);
            glUniformMatrix4fv(litMeshProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniformMatrix3fv(litMeshProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

            planeMesh.render();

            glUseProgram(0);
            glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
        }

        drawSphere(modelMatrix, new Vector3f(0.0f, 10.0f, 0.0f), 4.0f, MaterialNames.BLUE_SHINY, drawImposter[0]);
        drawSphereOrbit(modelMatrix, new Vector3f(0.0f, 10.0f, 0.0f), new Vector3f(0.6f, 0.8f, 0.0f), 20.0f,
                sphereTimer.getAlpha(), 2.0f, MaterialNames.DULL_GREY, drawImposter[1]);
        drawSphereOrbit(modelMatrix, new Vector3f(-10.0f, 1.0f, 0.0f), new Vector3f(0.0f, 1.0f, 0.0f), 10.0f,
                sphereTimer.getAlpha(), 1.0f, MaterialNames.BLACK_SHINY, drawImposter[2]);
        drawSphereOrbit(modelMatrix, new Vector3f(10.0f, 1.0f, 0.0f), new Vector3f(0.0f, 1.0f, 0.0f), 10.0f,
                sphereTimer.getAlpha() * 2.0f, 1.0f, MaterialNames.GOLD_METAL, drawImposter[3]);

        if (drawLights) {
            modelMatrix.pushMatrix();

            Vector4f v = calcLightPosition();
            modelMatrix.translate(new Vector3f(v.x, v.y, v.z));
            modelMatrix.scale(0.5f);

            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = new Vector4f(1.0f);
            glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            modelMatrix.identity();
            modelMatrix.translate(new Vector3f(0.0f, 0.0f, -viewPole.getView().radius));

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            cubeMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private ProgramMeshData litMeshProg;
    private ProgramImposData[] litImpProgs = new ProgramImposData[Impostors.NUM_IMPOSTORS.ordinal()];
    private String[] impShaderFileNames = new String[]{
            "BasicImpostor.vert", "BasicImpostor.frag",
            "PerspImpostor.vert", "PerspImpostor.frag",
            "DepthImpostor.vert", "DepthImpostor.frag"
    };
    private UnlitProgData unlit;

    private class ProgramMeshData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }

    private class ProgramImposData {
        int theProgram;

        int sphereRadiusUnif;
        int cameraSpherePosUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        litMeshProg = loadLitMeshProgram("PN.vert", "Lighting.frag");

        for (int progIndex = 0; progIndex < Impostors.NUM_IMPOSTORS.ordinal(); progIndex++) {
            litImpProgs[progIndex] = new ProgramImposData();
            litImpProgs[progIndex] = loadLitImposProgram(impShaderFileNames[progIndex * 2], impShaderFileNames[progIndex * 2 + 1]);
        }

        unlit = loadUnlitProgram("Unlit.vert", "Unlit.frag");
    }

    private ProgramMeshData loadLitMeshProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramMeshData data = new ProgramMeshData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private ProgramImposData loadLitImposProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramImposData data = new ProgramImposData();
        data.theProgram = Framework.createProgram(shaderList);
        data.sphereRadiusUnif = glGetUniformLocation(data.theProgram, "sphereRadius");
        data.cameraSpherePosUnif = glGetUniformLocation(data.theProgram, "cameraSpherePos");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh planeMesh;
    private Mesh sphereMesh;
    private Mesh cubeMesh;

    private int imposterVAO;

    private Timer sphereTimer = new Timer(Timer.Type.LOOP, 6.0f);

    private boolean[] drawImposter = {false, false, false, false};
    private boolean drawCameraPos;
    private boolean drawLights = true;


    private Vector4f calcLightPosition() {
        final float scale = 3.14159f * 2.0f;
        float timeThroughLoop = sphereTimer.getAlpha();

        float lightHeight = 20.0f;
        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(timeThroughLoop * scale) * 20.0f);
        lightPos.z = (float) (Math.sin(timeThroughLoop * scale) * 20.0f);
        return lightPos;
    }

    ////////////////////////////////
    // View setup.
    private MousePoles.ViewData initialViewData = new MousePoles.ViewData(
            new Vector3f(0.0f, 30.0f, 25.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            10.0f,
            0.0f
    );

    private MousePoles.ViewScale viewScale = new MousePoles.ViewScale(
            3.0f, 70.0f,
            3.5f, 1.5f,
            5.0f, 1.0f,
            90.0f / 250.0f
    );


    private MousePoles.ViewPole viewPole = new MousePoles.ViewPole(initialViewData, viewScale, MousePoles.MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private Impostors currImpostor = Impostors.BASIC;

    private enum Impostors {
        BASIC,
        PERSPECTIVE,
        DEPTH,

        NUM_IMPOSTORS
    }


    private void drawSphere(MatrixStackf modelMatrix, Vector3f position, float radius, MaterialNames material, boolean drawImposter) {
        glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer,
                material.ordinal() * materialBlockOffset, MaterialBlock.BYTES);

        if (drawImposter) {
            Vector4f cameraSpherePos = new Matrix4f(modelMatrix).transform(new Vector4f(position, 1.0f));
            glUseProgram(litImpProgs[currImpostor.ordinal()].theProgram);
            glUniform3fv(litImpProgs[currImpostor.ordinal()].cameraSpherePosUnif, cameraSpherePos.get(vec4Buffer));
            glUniform1f(litImpProgs[currImpostor.ordinal()].sphereRadiusUnif, radius);

            glBindVertexArray(imposterVAO);
            glEnableVertexAttribArray(0);
            glVertexAttribPointer(0, 1, GL_FLOAT, false, 0, 0);

            glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

            glBindVertexArray(0);
            glUseProgram(0);
        } else {
            modelMatrix.pushMatrix();

            modelMatrix.translate(position);
            modelMatrix.scale(radius * 2.0f);  // The unit sphere has a radius 0.5f.

            Matrix3f normMatrix = new Matrix3f(modelMatrix);
            normMatrix.invert().transpose();

            glUseProgram(litMeshProg.theProgram);
            glUniformMatrix4fv(litMeshProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniformMatrix3fv(litMeshProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

            sphereMesh.render("lit");

            glUseProgram(0);

            modelMatrix.popMatrix();
        }

        glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
    }

    private void drawSphereOrbit(MatrixStackf modelMatrix, Vector3f orbitCenter, Vector3f orbitAxis,
                                 float orbitRadius, float orbitAlpha, float sphereRadius, MaterialNames material,
                                 boolean drawImposter) {
        modelMatrix.pushMatrix();

        modelMatrix.translate(orbitCenter);
        modelMatrix.rotate((float) Math.toRadians(360.0f * orbitAlpha), orbitAxis);

        Vector3f offsetDir = new Vector3f(orbitAxis).cross(new Vector3f(0.0f, 1.0f, 0.0f));
        if (offsetDir.length() < 0.001f) {
            offsetDir = new Vector3f(orbitAxis).cross(new Vector3f(1.0f, 0.0f, 0.0f));
        }

        offsetDir.normalize();

        modelMatrix.translate(offsetDir.mul(orbitRadius));

        drawSphere(modelMatrix, new Vector3f(0.0f), sphereRadius, material, drawImposter);

        modelMatrix.popMatrix();
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    ////////////////////////////////
    private final int materialBlockIndex = 0;

    private int materialUniformBuffer;
    private int materialBlockOffset;

    private enum MaterialNames {
        TERRAIN,
        BLUE_SHINY,
        GOLD_METAL,
        DULL_GREY,
        BLACK_SHINY,

        NUM_MATERIALS
    }


    private void createMaterials() {
        UniformBlockArray<MaterialBlock> ubArray = new UniformBlockArray<>(MaterialBlock.BYTES, MaterialNames.NUM_MATERIALS.ordinal());
        materialBlockOffset = ubArray.getArrayOffset();

        MaterialBlock matBlock = new MaterialBlock();
        matBlock.diffuseColor = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
        matBlock.specularColor = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
        matBlock.specularShininess = 0.6f;
        ubArray.set(MaterialNames.TERRAIN.ordinal(), matBlock);

        matBlock.diffuseColor = new Vector4f(0.1f, 0.1f, 0.8f, 1.0f);
        matBlock.specularColor = new Vector4f(0.8f, 0.8f, 0.8f, 1.0f);
        matBlock.specularShininess = 0.1f;
        ubArray.set(MaterialNames.BLUE_SHINY.ordinal(), matBlock);

        matBlock.diffuseColor = new Vector4f(0.803f, 0.709f, 0.15f, 1.0f);
        matBlock.specularColor = new Vector4f(0.803f, 0.709f, 0.15f, 1.0f).mul(0.75f);
        matBlock.specularShininess = 0.18f;
        ubArray.set(MaterialNames.GOLD_METAL.ordinal(), matBlock);

        matBlock.diffuseColor = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);
        matBlock.specularColor = new Vector4f(0.1f, 0.1f, 0.1f, 1.0f);
        matBlock.specularShininess = 0.8f;
        ubArray.set(MaterialNames.DULL_GREY.ordinal(), matBlock);

        matBlock.diffuseColor = new Vector4f(0.05f, 0.05f, 0.05f, 1.0f);
        matBlock.specularColor = new Vector4f(0.95f, 0.95f, 0.95f, 1.0f);
        matBlock.specularShininess = 0.3f;
        ubArray.set(MaterialNames.BLACK_SHINY.ordinal(), matBlock);

        materialUniformBuffer = ubArray.createBufferObject();
    }
} public class GeomImpostor extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut13/data/";
        new GeomImpostor().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        planeMesh = new Mesh("LargePlane.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        imposterVBO = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, imposterVBO);
        glBufferData(GL_ARRAY_BUFFER, NUMBER_OF_SPHERES * 4 * Float.BYTES, GL_STREAM_DRAW);

        imposterVAO = glGenVertexArrays();
        glBindVertexArray(imposterVAO);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 16, 0);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, false, 16, 12);

        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        glEnable(GL_PROGRAM_POINT_SIZE);

        createMaterials();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        sphereTimer.togglePause();
                        break;

                    case GLFW_KEY_MINUS:
                        sphereTimer.rewind(0.5f);
                        break;

                    case GLFW_KEY_EQUAL:
                        sphereTimer.fastForward(0.5f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        drawLights = !drawLights;
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        sphereTimer.update(elapsedTime);

        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());
        final Matrix4f worldToCamMat = modelMatrix;

        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        float halfLightDistance = 25.0f;
        lightData.lightAttenuation = 1.0f / (halfLightDistance * halfLightDistance);

        lightData.lights[0] = new PerLight();
        lightData.lights[0].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(0.707f, 0.707f, 0.0f, 0.0f));
        lightData.lights[0].lightIntensity = new Vector4f(0.6f, 0.6f, 0.6f, 1.0f);

        lightData.lights[1] = new PerLight();
        lightData.lights[1].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(calcLightPosition()));
        lightData.lights[1].lightIntensity = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialTerrainUniformBuffer, 0, MaterialBlock.BYTES);

            Matrix3f normMatrix = new Matrix3f(modelMatrix);
            normMatrix.invert().transpose();

            glUseProgram(litMeshProg.theProgram);
            glUniformMatrix4fv(litMeshProg.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniformMatrix3fv(litMeshProg.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

            planeMesh.render();

            glUseProgram(0);
            glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
        }

        {
            VertexData[] posSizeArray = new VertexData[NUMBER_OF_SPHERES];

            posSizeArray[0] = new VertexData();
            Vector4f tmp = worldToCamMat.transform(new Vector4f(0.0f, 10.0f, 0.0f, 1.0f));
            posSizeArray[0].cameraPosition = new Vector3f(tmp.x, tmp.y, tmp.z);
            posSizeArray[0].sphereRadius = 4.0f;

            posSizeArray[1] = new VertexData();
            posSizeArray[1].cameraPosition = getSphereOrbitPos(modelMatrix, new Vector3f(0.0f, 10.0f, 0.0f),
                    new Vector3f(0.6f, 0.8f, 0.0f), 20.0f, sphereTimer.getAlpha());
            posSizeArray[1].sphereRadius = 2.0f;

            posSizeArray[2] = new VertexData();
            posSizeArray[2].cameraPosition = getSphereOrbitPos(modelMatrix, new Vector3f(-10.0f, 1.0f, 0.0f),
                    new Vector3f(0.0f, 1.0f, 0.0f), 10.0f, sphereTimer.getAlpha());
            posSizeArray[2].sphereRadius = 1.0f;

            posSizeArray[3] = new VertexData();
            posSizeArray[3].cameraPosition = getSphereOrbitPos(modelMatrix, new Vector3f(10.0f, 1.0f, 0.0f),
                    new Vector3f(0.0f, 1.0f, 0.0f), 10.0f, sphereTimer.getAlpha() * 2.0f);
            posSizeArray[3].sphereRadius = 1.0f;

            glBindBuffer(GL_ARRAY_BUFFER, imposterVBO);

            {
                ByteBuffer vertexDataBuffer = BufferUtils.createByteBuffer(NUMBER_OF_SPHERES * VertexData.BYTES);

                for (VertexData vertexData : posSizeArray) {
                    vertexData.get(vertexDataBuffer);
                }

                vertexDataBuffer.flip();

                glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STREAM_DRAW);
            }

            glBindBuffer(GL_ARRAY_BUFFER, 0);
        }

        {
            glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialArrayUniformBuffer, 0, MaterialBlock.BYTES * NUMBER_OF_SPHERES);

            glUseProgram(litImpProg.theProgram);
            glBindVertexArray(imposterVAO);
            glDrawArrays(GL_POINTS, 0, NUMBER_OF_SPHERES);
            glBindVertexArray(0);
            glUseProgram(0);

            glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
        }

        if (drawLights) {
            modelMatrix.pushMatrix();

            Vector4f tmp = calcLightPosition();
            modelMatrix.translate(new Vector3f(tmp.x, tmp.y, tmp.z));
            modelMatrix.scale(0.5f);

            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = new Vector4f(1.0f);
            glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            modelMatrix.identity();
            modelMatrix.translate(new Vector3f(0.0f, 0.0f, -viewPole.getView().radius));

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            cubeMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private ProgramMeshData litMeshProg;
    private ProgramImposData litImpProg;
    private UnlitProgData unlit;

    private class ProgramMeshData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }

    private class ProgramImposData {
        int theProgram;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        litMeshProg = loadLitMeshProgram("PN.vert", "Lighting.frag");
        litImpProg = loadLitImposProgram("GeomImpostor.vert", "GeomImpostor.geom", "GeomImpostor.frag");
        unlit = loadUnlitProgram("Unlit.vert", "Unlit.frag");
    }

    private ProgramMeshData loadLitMeshProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramMeshData data = new ProgramMeshData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private ProgramImposData loadLitImposProgram(String vertexShaderFileName, String geometryShaderFileName,
                                                 String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_GEOMETRY_SHADER, geometryShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramImposData data = new ProgramImposData();
        data.theProgram = Framework.createProgram(shaderList);

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh planeMesh;
    private Mesh cubeMesh;

    private int imposterVAO;
    private int imposterVBO;

    private final int NUMBER_OF_SPHERES = 4;

    private Timer sphereTimer = new Timer(Timer.Type.LOOP, 6.0f);

    private boolean drawCameraPos;
    private boolean drawLights = true;


    private Vector4f calcLightPosition() {
        final float scale = 3.14159f * 2.0f;
        float timeThroughLoop = sphereTimer.getAlpha();

        float lightHeight = 20.0f;
        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        lightPos.x = (float) (Math.cos(timeThroughLoop * scale) * 20.0f);
        lightPos.z = (float) (Math.sin(timeThroughLoop * scale) * 20.0f);
        return lightPos;
    }


    private Vector3f getSphereOrbitPos(MatrixStackf modelMatrix, Vector3f orbitCenter, Vector3f orbitAxis, float orbitRadius, float orbitAlpha) {
        modelMatrix.pushMatrix();

        modelMatrix.translate(orbitCenter);
        modelMatrix.rotate((float) Math.toRadians(360.0f * orbitAlpha), orbitAxis);

        Vector3f offsetDir = new Vector3f(orbitAxis).cross(new Vector3f(0.0f, 1.0f, 0.0f));
        if (offsetDir.length() < 0.001f) {
            offsetDir = new Vector3f(orbitAxis).cross(new Vector3f(1.0f, 0.0f, 0.0f));
        }

        offsetDir.normalize();

        modelMatrix.translate(offsetDir.mul(orbitRadius));

        Vector4f tmp = modelMatrix.transform(new Vector4f(0.0f, 0.0f, 0.0f, 1.0f));
        Vector3f res = new Vector3f(tmp.x, tmp.y, tmp.z);
        modelMatrix.popMatrix();
        return res;
    }

    ////////////////////////////////
    // View setup.
    private ViewData initialViewData = new ViewData(
            new Vector3f(0.0f, 30.0f, 25.0f),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            10.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            3.0f, 70.0f,
            3.5f, 1.5f,
            5.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private class VertexData implements Bufferable {
        static final int BYTES = Float.BYTES * (3 + 1);

        Vector3f cameraPosition;
        float sphereRadius;

        @Override
        public ByteBuffer get(ByteBuffer buffer) {
            buffer.putFloat(cameraPosition.x);
            buffer.putFloat(cameraPosition.y);
            buffer.putFloat(cameraPosition.z);
            buffer.putFloat(sphereRadius);
            return buffer;
        }
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    ////////////////////////////////
    private final int materialBlockIndex = 0;

    private int materialArrayUniformBuffer;
    private int materialTerrainUniformBuffer;


    private void createMaterials() {
        ArrayList<MaterialBlock> ubArray = new ArrayList<>();

        MaterialBlock matEntry = new MaterialBlock();
        matEntry.diffuseColor = new Vector4f(0.1f, 0.1f, 0.8f, 1.0f);
        matEntry.specularColor = new Vector4f(0.8f, 0.8f, 0.8f, 1.0f);
        matEntry.specularShininess = 0.1f;
        ubArray.add(matEntry);

        matEntry = new MaterialBlock();
        matEntry.diffuseColor = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);
        matEntry.specularColor = new Vector4f(0.1f, 0.1f, 0.1f, 1.0f);
        matEntry.specularShininess = 0.8f;
        ubArray.add(matEntry);

        matEntry = new MaterialBlock();
        matEntry.diffuseColor = new Vector4f(0.05f, 0.05f, 0.05f, 1.0f);
        matEntry.specularColor = new Vector4f(0.95f, 0.95f, 0.95f, 1.0f);
        matEntry.specularShininess = 0.3f;
        ubArray.add(matEntry);

        matEntry = new MaterialBlock();
        matEntry.diffuseColor = new Vector4f(0.803f, 0.709f, 0.15f, 1.0f);
        matEntry.specularColor = new Vector4f(0.803f, 0.709f, 0.15f, 1.0f).mul(0.75f);
        matEntry.specularShininess = 0.18f;
        ubArray.add(matEntry);

        materialArrayUniformBuffer = glGenBuffers();
        materialTerrainUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, materialArrayUniformBuffer);

        {
            ByteBuffer ubArrayBuffer = BufferUtils.createByteBuffer(ubArray.size() * MaterialBlock.BYTES);

            for (MaterialBlock anUbArray : ubArray) {
                anUbArray.get(ubArrayBuffer);
            }

            ubArrayBuffer.flip();

            glBufferData(GL_UNIFORM_BUFFER, ubArrayBuffer, GL_STATIC_DRAW);
        }

        glBindBuffer(GL_UNIFORM_BUFFER, materialTerrainUniformBuffer);

        {
            matEntry = new MaterialBlock();
            matEntry.diffuseColor = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
            matEntry.specularColor = new Vector4f(0.5f, 0.5f, 0.5f, 1.0f);
            matEntry.specularShininess = 0.6f;

            glBufferData(GL_UNIFORM_BUFFER, matEntry.getAndFlip(BufferUtils.createByteBuffer(MaterialBlock.BYTES)),
                    GL_STATIC_DRAW);
        }

        glBindBuffer(GL_UNIFORM_BUFFER, 0);
    }
} public class BasicTexture extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut14/data/";
        new BasicTexture().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        objectMesh = new Mesh("Infinity.xml");
        cubeMesh = new Mesh("UnitCube.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        MaterialBlock matBlock = new MaterialBlock();
        matBlock.diffuseColor = new Vector4f(1.0f, 0.673f, 0.043f, 1.0f);
        matBlock.specularColor = new Vector4f(1.0f, 0.673f, 0.043f, 1.0f).mul(0.4f);
        matBlock.specularShininess = specularShininess;

        materialUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, materialUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, matBlock.getAndFlip(BufferUtils.createByteBuffer(12 * 4)), GL_STATIC_DRAW);

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, 0, MaterialBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        createGaussianTextures();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_MINUS:
                        lightTimer.rewind(0.5f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lightTimer.fastForward(0.5f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        drawLights = !drawLights;
                        break;

                    case GLFW_KEY_SPACE:
                        useTexture = !useTexture;
                        if (useTexture) {
                            System.out.printf("Texture\n");
                        } else {
                            System.out.printf("Shader\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());
        final Matrix4f worldToCamMat = modelMatrix;

        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        float halfLightDistance = 25.0f;
        lightData.lightAttenuation = 1.0f / (halfLightDistance * halfLightDistance);

        Vector3f globalLightDirection = new Vector3f(0.707f, 0.707f, 0.0f);

        lightData.lights[0] = new PerLight();
        lightData.lights[0].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(globalLightDirection, 0.0f));
        lightData.lights[0].lightIntensity = new Vector4f(0.6f, 0.6f, 0.6f, 1.0f);

        lightData.lights[1] = new PerLight();
        lightData.lights[1].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(calcLightPosition()));
        lightData.lights[1].lightIntensity = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, 0, MaterialBlock.BYTES);

            modelMatrix.pushMatrix();

            modelMatrix.mul(objtPole.calcMatrix());
            modelMatrix.scale(2.0f);

            Matrix3f normMatrix = new Matrix3f(modelMatrix);
            normMatrix.invert().transpose();

            ProgramData prog = useTexture ? litTextureProg : litShaderProg;

            glUseProgram(prog.theProgram);
            glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniformMatrix3fv(prog.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

            glActiveTexture(GL_TEXTURE0 + gaussTexUnit);
            glBindTexture(GL_TEXTURE_1D, gaussTextures[currTexture]);
            glBindSampler(gaussTexUnit, gaussSampler);

            objectMesh.render("lit");

            glBindSampler(gaussTexUnit, 0);
            glBindTexture(GL_TEXTURE_1D, 0);

            glUseProgram(0);
            glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);

            modelMatrix.popMatrix();
        }

        if (drawLights) {
            modelMatrix.pushMatrix();

            Vector4f tmp = calcLightPosition();
            modelMatrix.translate(new Vector3f(tmp.x, tmp.y, tmp.z));
            modelMatrix.scale(0.25f);

            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = new Vector4f(1.0f);
            glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
            cubeMesh.render("flat");

            modelMatrix.popMatrix();

            modelMatrix.translate(globalLightDirection.mul(100.0f));
            modelMatrix.scale(5.0f);

            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeMesh.render("flat");

            glUseProgram(0);
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            modelMatrix.identity();
            modelMatrix.translate(new Vector3f(0.0f, 0.0f, -viewPole.getView().radius));
            modelMatrix.scale(0.25f);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            cubeMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData litShaderProg;
    private ProgramData litTextureProg;
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        litShaderProg = loadStandardProgram("PN.vert", "ShaderGaussian.frag");
        litTextureProg = loadStandardProgram("PN.vert", "TextureGaussian.frag");
        unlit = loadUnlitProgram("Unlit.vert", "Unlit.frag");
    }

    private ProgramData loadStandardProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int gaussianTextureUnif = glGetUniformLocation(data.theProgram, "gaussianTexture");
        glUseProgram(data.theProgram);
        glUniform1i(gaussianTextureUnif, gaussTexUnit);
        glUseProgram(0);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh objectMesh;
    private Mesh cubeMesh;

    private float specularShininess = 0.2f;

    private final int gaussTexUnit = 0;

    private final int NUM_GAUSS_TEXTURES = 4;
    private int[] gaussTextures = new int[NUM_GAUSS_TEXTURES];
    private int currTexture;

    private int gaussSampler;

    private Timer lightTimer = new Timer(Timer.Type.LOOP, 6.0f);

    private boolean drawLights = true;
    private boolean drawCameraPos;
    private boolean useTexture;


    private void createGaussianTextures() {
        for (int textureIndex = 0; textureIndex < NUM_GAUSS_TEXTURES; textureIndex++) {
            int cosAngleResolution = calcCosAngResolution(textureIndex);
            gaussTextures[textureIndex] = createGaussianTexture(cosAngleResolution);
        }

        gaussSampler = glGenSamplers();
        glSamplerParameteri(gaussSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glSamplerParameteri(gaussSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glSamplerParameteri(gaussSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    }

    private int createGaussianTexture(int cosAngleResolution) {
        byte[] textureData = new byte[cosAngleResolution];

        buildGaussianData(textureData, cosAngleResolution);

        ByteBuffer textureDataBuffer = BufferUtils.createByteBuffer(textureData.length);
        textureDataBuffer.put(textureData);
        textureDataBuffer.flip();

        int gaussTexture = glGenTextures();
        glBindTexture(GL_TEXTURE_1D, gaussTexture);
        glTexImage1D(GL_TEXTURE_1D, 0, GL_R8, cosAngleResolution, 0, GL_RED, GL_UNSIGNED_BYTE, textureDataBuffer);
        glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_BASE_LEVEL, 0);
        glTexParameteri(GL_TEXTURE_1D, GL_TEXTURE_MAX_LEVEL, 0);
        glBindTexture(GL_TEXTURE_1D, 0);

        return gaussTexture;
    }

    private void buildGaussianData(byte[] textureData, int cosAngleResolution) {
        for (int cosAngIndex = 0; cosAngIndex < cosAngleResolution; cosAngIndex++) {
            float cosAng = cosAngIndex / (float) (cosAngleResolution - 1);
            float angle = (float) Math.acos(cosAng);
            float exponent = angle / specularShininess;
            exponent = -(exponent * exponent);
            float gaussianTerm = (float) Math.exp(exponent);

            textureData[cosAngIndex] = (byte) (gaussianTerm * 255.0f);
        }
    }

    private int calcCosAngResolution(int level) {
        final int cosAngleStart = 64;
        return cosAngleStart * (int) (Math.pow(2.0f, level));
    }


    private Vector4f calcLightPosition() {
        final float scale = 3.14159f * 2.0f;
        float timeThroughLoop = lightTimer.getAlpha();

        float lightHeight = 1.0f;
        Vector4f lightPos = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        float lightRadius = 3.0f;
        lightPos.x = (float) (Math.cos(timeThroughLoop * scale) * lightRadius);
        lightPos.z = (float) (Math.sin(timeThroughLoop * scale) * lightRadius);
        return lightPos;
    }

    ////////////////////////////////
    // View / Object setup.
    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewData initialViewData = new ViewData(
            new Vector3f(initialObjectData.position),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            10.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            1.5f, 70.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    ////////////////////////////////
    private final int materialBlockIndex = 0;

    private int materialUniformBuffer;
} public class MaterialTexture extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut14/data/";
        new MaterialTexture().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        objectMesh = new Mesh("Infinity.xml");
        cubeMesh = new Mesh("UnitCube.xml");
        planeMesh = new Mesh("UnitPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        setupMaterials();

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_DYNAMIC_DRAW);

        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        // Bind the static buffers.
        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, 0, MaterialBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        createGaussianTextures();
        createShininessTexture();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_P:
                        lightTimer.togglePause();
                        break;

                    case GLFW_KEY_MINUS:
                        lightTimer.rewind(0.5f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lightTimer.fastForward(0.5f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        drawLights = !drawLights;
                        break;

                    case GLFW_KEY_Y:
                        useInfinity = !useInfinity;
                        break;

                    case GLFW_KEY_SPACE:
                        int index = (shaderMode.ordinal() + 1) % ShaderMode.NUM_SHADER_MODES.ordinal();
                        shaderMode = ShaderMode.values()[index];
                        System.out.printf("%s\n", shaderModeNames[shaderMode.ordinal()]);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }

                if (GLFW_KEY_1 <= key && key <= GLFW_KEY_9) {
                    int number = key - GLFW_KEY_1;
                    if (number < NUM_GAUSS_TEXTURES) {
                        System.out.printf("Angle Resolution: %d\n", calcCosAngResolution(number));
                        currTexture = number;
                    }

                    if (number >= (9 - NUM_MATERIALS)) {
                        number = number - (9 - NUM_MATERIALS);
                        System.out.printf("Material number %d\n", number);
                        currMaterial = number;
                    }
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, objtPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(objtPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, objtPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        lightTimer.update(elapsedTime);

        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());
        final Matrix4f worldToCamMat = modelMatrix;

        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        float halfLightDistance = 25.0f;
        lightData.lightAttenuation = 1.0f / (halfLightDistance * halfLightDistance);

        Vector3f globalLightDirection = new Vector3f(0.707f, 0.707f, 0.0f);

        lightData.lights[0] = new PerLight();
        lightData.lights[0].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(globalLightDirection, 0.0f));
        lightData.lights[0].lightIntensity = new Vector4f(0.6f, 0.6f, 0.6f, 1.0f);

        lightData.lights[1] = new PerLight();
        lightData.lights[1].cameraSpaceLightPos = worldToCamMat.transform(new Vector4f(calcLightPosition()));
        lightData.lights[1].lightIntensity = new Vector4f(0.4f, 0.4f, 0.4f, 1.0f);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, lightData.getAndFlip(lightBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        {
            Mesh mesh = useInfinity ? objectMesh : planeMesh;

            glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, currMaterial * materialOffset,
                    MaterialBlock.BYTES);

            modelMatrix.pushMatrix();

            modelMatrix.mul(objtPole.calcMatrix());
            modelMatrix.scale(useInfinity ? 2.0f : 4.0f);

            Matrix3f normMatrix = new Matrix3f(modelMatrix);
            normMatrix.invert().transpose();

            ProgramData prog = programs[shaderMode.ordinal()];

            glUseProgram(prog.theProgram);
            glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniformMatrix3fv(prog.normalModelToCameraMatrixUnif, false, normMatrix.get(mat3Buffer));

            glActiveTexture(GL_TEXTURE0 + gaussTexUnit);
            glBindTexture(GL_TEXTURE_2D, gaussTextures[currTexture]);
            glBindSampler(gaussTexUnit, textureSampler);

            glActiveTexture(GL_TEXTURE0 + shineTexUnit);
            glBindTexture(GL_TEXTURE_2D, shineTexture);
            glBindSampler(shineTexUnit, textureSampler);

            if (shaderMode != ShaderMode.FIXED) {
                mesh.render("lit-tex");
            } else {
                mesh.render("lit");
            }

            glBindSampler(gaussTexUnit, 0);
            glBindTexture(GL_TEXTURE_2D, 0);

            glUseProgram(0);
            glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);

            modelMatrix.popMatrix();
        }

        if (drawLights) {
            modelMatrix.pushMatrix();

            Vector4f tmp = calcLightPosition();
            modelMatrix.translate(new Vector3f(tmp.x, tmp.y, tmp.z));
            modelMatrix.scale(0.25f);

            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = new Vector4f(1.0f);
            glUniform4fv(unlit.objectColorUnif, lightColor.get(vec4Buffer));
            cubeMesh.render("flat");

            modelMatrix.popMatrix();

            modelMatrix.translate(globalLightDirection.mul(100.0f));
            modelMatrix.scale(5.0f);

            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            cubeMesh.render("flat");

            glUseProgram(0);
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            modelMatrix.identity();
            modelMatrix.translate(new Vector3f(0.0f, 0.0f, -viewPole.getView().radius));
            modelMatrix.scale(0.25f);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlit.theProgram);
            glUniformMatrix4fv(unlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            cubeMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            cubeMesh.render("flat");

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(45.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData[] programs = new ProgramData[ShaderMode.NUM_SHADER_MODES.ordinal()];
    private ShaderPairs[] shaderPairs = new ShaderPairs[]{
            new ShaderPairs("PN.vert", "FixedShininess.frag"),
            new ShaderPairs("PNT.vert", "TextureShininess.frag"),
            new ShaderPairs("PNT.vert", "TextureCompute.frag")
    };
    private UnlitProgData unlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int normalModelToCameraMatrixUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int objectColorUnif;
        int modelToCameraMatrixUnif;
    }

    private class ShaderPairs {
        String vertShaderFileName;
        String fragShaderFileName;

        ShaderPairs(String vertShader, String fragShader) {
            this.vertShaderFileName = vertShader;
            this.fragShaderFileName = fragShader;
        }
    }


    private void initializePrograms() {
        for (int progIndex = 0; progIndex < ShaderMode.NUM_SHADER_MODES.ordinal(); progIndex++) {
            programs[progIndex] = loadStandardProgram(shaderPairs[progIndex].vertShaderFileName,
                    shaderPairs[progIndex].fragShaderFileName);
        }

        unlit = loadUnlitProgram("Unlit.vert", "Unlit.frag");
    }

    private ProgramData loadStandardProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.normalModelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "normalModelToCameraMatrix");

        int materialBlock = glGetUniformBlockIndex(data.theProgram, "Material");
        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");

        glUniformBlockBinding(data.theProgram, materialBlock, materialBlockIndex);
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int gaussianTextureUnif = glGetUniformLocation(data.theProgram, "gaussianTexture");
        int shininessTextureUnif = glGetUniformLocation(data.theProgram, "shininessTexture");
        glUseProgram(data.theProgram);
        glUniform1i(gaussianTextureUnif, gaussTexUnit);
        glUniform1i(shininessTextureUnif, shineTexUnit);
        glUseProgram(0);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh objectMesh;
    private Mesh cubeMesh;
    private Mesh planeMesh;

    private final int gaussTexUnit = 0;
    private final int shineTexUnit = 1;

    private final int NUM_GAUSS_TEXTURES = 4;
    private int gaussTextures[] = new int[NUM_GAUSS_TEXTURES];
    private int currTexture = NUM_GAUSS_TEXTURES - 1;

    private int textureSampler;
    private int shineTexture;
    private int materialOffset;
    private int currMaterial;

    private final String[] shaderModeNames = {
            "Fixed Shininess with Gaussian Texture",
            "Texture Shininess with Gaussian Texture",
            "Texture Shininess with computed Gaussian"
    };

    private Timer lightTimer = new Timer(Timer.Type.LOOP, 6.0f);

    private boolean drawLights = true;
    private boolean useInfinity = true;
    private boolean drawCameraPos;

    private ShaderMode shaderMode = ShaderMode.FIXED;

    private enum ShaderMode {
        FIXED,
        TEXTURED,
        TEXTURED_COMPUTE,

        NUM_SHADER_MODES
    }


    private void createGaussianTextures() {
        for (int textureIndex = 0; textureIndex < NUM_GAUSS_TEXTURES; textureIndex++) {
            int cosAngleResolution = calcCosAngResolution(textureIndex);
            gaussTextures[textureIndex] = createGaussianTexture(cosAngleResolution, 128);
        }

        textureSampler = glGenSamplers();
        glSamplerParameteri(textureSampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glSamplerParameteri(textureSampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glSamplerParameteri(textureSampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(textureSampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    }

    private int createGaussianTexture(int cosAngleResolution, int shininessResolution) {
        byte[] textureData = new byte[shininessResolution * cosAngleResolution];

        buildGaussianData(textureData, cosAngleResolution, shininessResolution);

        ByteBuffer textureDataBuffer = BufferUtils.createByteBuffer(textureData.length);
        textureDataBuffer.put(textureData);
        textureDataBuffer.flip();

        int gaussTexture = glGenTextures();
        glBindTexture(GL_TEXTURE_2D, gaussTexture);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, cosAngleResolution, shininessResolution, 0, GL_RED, GL_UNSIGNED_BYTE, textureDataBuffer);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
        glBindTexture(GL_TEXTURE_2D, 0);

        return gaussTexture;
    }

    private void buildGaussianData(byte[] textureData, int cosAngleResolution, int shininessResolution) {
        int offset = 0;
        for (int shinIndex = 1; shinIndex <= shininessResolution; shinIndex++) {
            float shininess = shinIndex / (float) (shininessResolution);

            for (int cosAngIndex = 0; cosAngIndex < cosAngleResolution; cosAngIndex++) {
                float cosAng = cosAngIndex / (float) (cosAngleResolution - 1);
                float angle = (float) Math.acos(cosAng);
                float exponent = angle / shininess;
                exponent = -(exponent * exponent);
                float gaussianTerm = (float) Math.exp(exponent);

                textureData[offset] = (byte) (char) (gaussianTerm * 255.0f);
                offset++;
            }
        }
    }

    private int calcCosAngResolution(int level) {
        final int cosAngleStart = 64;
        return cosAngleStart * (int) (Math.pow(2.0f, level));
    }

    private void createShininessTexture() {
        try {
            String filePath = Framework.findFileOrThrow("main.dds");
            ImageSet imageSet = DdsLoader.loadFromFile(filePath);

            SingleImage image = imageSet.getImage(0, 0, 0);
            Dimensions dims = image.getDimensions();

            shineTexture = glGenTextures();
            glBindTexture(GL_TEXTURE_2D, shineTexture);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, dims.width, dims.height, 0, GL_RED, GL_UNSIGNED_BYTE, image.getImageData());
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
            glBindTexture(GL_TEXTURE_2D, 0);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }


    private Vector4f calcLightPosition() {
        final float scale = 3.14159f * 2.0f;
        float timeThroughLoop = lightTimer.getAlpha();

        float lightHeight = 1.0f;
        Vector4f ret = new Vector4f(0.0f, lightHeight, 0.0f, 1.0f);
        float lightRadius = 3.0f;
        ret.x = (float) (Math.cos(timeThroughLoop * scale) * lightRadius);
        ret.z = (float) (Math.sin(timeThroughLoop * scale) * lightRadius);
        return ret;
    }

    ////////////////////////////////
    // View / Object setup.
    private ObjectData initialObjectData = new ObjectData(
            new Vector3f(0.0f, 0.5f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewData initialViewData = new ViewData(
            new Vector3f(initialObjectData.position),
            new Quaternionf(0.3826834f, 0.0f, 0.0f, 0.92387953f),
            10.0f,
            0.0f
    );

    private ViewScale viewScale = new ViewScale(
            1.5f, 70.0f,
            1.5f, 0.5f,
            0.0f, 0.0f,  // No camera movement.
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialViewData, viewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole objtPole = new ObjectPole(initialObjectData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final int projectionBlockIndex = 2;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;

    ////////////////////////////////
    private final int NUM_MATERIALS = 2;

    private final int materialBlockIndex = 0;

    private int materialUniformBuffer;


    private void setupMaterials() {
        UniformBlockArray<MaterialBlock> ubArray = new UniformBlockArray<>(MaterialBlock.BYTES, NUM_MATERIALS);
        MaterialBlock matBlock;

        matBlock = new MaterialBlock();
        matBlock.diffuseColor = new Vector4f(1.0f, 0.673f, 0.043f, 1.0f);
        matBlock.specularColor = new Vector4f(1.0f, 0.673f, 0.043f, 1.0f).mul(0.4f);
        matBlock.specularShininess = 0.125f;
        ubArray.set(0, matBlock);

        matBlock = new MaterialBlock();
        matBlock.diffuseColor = new Vector4f(0.01f, 0.01f, 0.01f, 1.0f);
        matBlock.specularColor = new Vector4f(0.99f, 0.99f, 0.99f, 1.0f);
        matBlock.specularShininess = 0.125f;
        ubArray.set(1, matBlock);

        materialUniformBuffer = ubArray.createBufferObject();
        materialOffset = ubArray.getArrayOffset();
    }
} public class PerspectiveInterpolation extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut14/data/";
        new PerspectiveInterpolation().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        realHallway = new Mesh("RealHallway.xml");
        fauxHallway = new Mesh("FauxHallway.xml");


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_S:
                        useFakeHallway = !useFakeHallway;
                        if (useFakeHallway) {
                            System.out.printf("Fake Hallway.\n");
                        } else {
                            System.out.printf("Real Hallway.\n");
                        }
                        break;

                    case GLFW_KEY_P:
                        useSmoothInterpolation = !useSmoothInterpolation;
                        if (useSmoothInterpolation) {
                            System.out.printf("Perspective correct interpolation.\n");
                        } else {
                            System.out.printf("Just linear interpolation.\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        if (useSmoothInterpolation) {
            glUseProgram(smoothInterp.theProgram);
        } else {
            glUseProgram(linearInterp.theProgram);
        }

        if (useFakeHallway) {
            fauxHallway.render();
        } else {
            realHallway.render();
        }

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData smoothInterp;
    private ProgramData linearInterp;

    private class ProgramData {
        int theProgram;

        int cameraToClipMatrixUnif;
    }


    private void initializePrograms() {
        smoothInterp = loadProgram("SmoothVertexColors.vert", "SmoothVertexColors.frag");
        linearInterp = loadProgram("NoCorrectVertexColors.vert", "NoCorrectVertexColors.frag");

        float zNear = 1.0f;
        float zFar = 1000.0f;
        MatrixStackf persMatrix = new MatrixStackf();
        persMatrix.perspective((float) Math.toRadians(60.0f), 1.0f, zNear, zFar);

        glUseProgram(smoothInterp.theProgram);
        glUniformMatrix4fv(smoothInterp.cameraToClipMatrixUnif, false, persMatrix.get(mat4Buffer));
        glUseProgram(linearInterp.theProgram);
        glUniformMatrix4fv(linearInterp.cameraToClipMatrixUnif, false, persMatrix.get(mat4Buffer));
        glUseProgram(0);
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.cameraToClipMatrixUnif = glGetUniformLocation(data.theProgram, "cameraToClipMatrix");
        return data;
    }

    ////////////////////////////////
    private Mesh realHallway;
    private Mesh fauxHallway;

    private boolean useSmoothInterpolation = true;
    private boolean useFakeHallway;
} public class ManyImages extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut15/data/";
        new ManyImages().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        corridor = new Mesh("Corridor.xml");
        plane = new Mesh("BigPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        loadCheckerTexture();
        loadMipmapTexture();
        createSamplers();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        useMipmapTexture = !useMipmapTexture;
                        break;

                    case GLFW_KEY_Y:
                        drawCorridor = !drawCorridor;
                        break;

                    case GLFW_KEY_P:
                        camTimer.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }

                if (GLFW_KEY_1 <= key && key <= GLFW_KEY_9) {
                    int number = key - GLFW_KEY_1;
                    if (number < NUM_SAMPLERS) {
                        System.out.printf("Sampler: %s\n", samplerNames[number]);
                        currSampler = number;
                    }
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        camTimer.update(elapsedTime);

        float cyclicAngle = camTimer.getAlpha() * 6.28f;
        float hOffset = (float) (Math.cos(cyclicAngle) * 0.25f);
        float vOffset = (float) (Math.sin(cyclicAngle) * 0.25f);

        MatrixStackf modelMatrix = new MatrixStackf(10);

        Vector3f eye = new Vector3f(hOffset, 1.0f, -64.0f);
        Vector3f center = new Vector3f(hOffset, -5.0f + vOffset, -44.0f);
        Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
        final Matrix4f worldToCamMat = new Matrix4f().lookAt(eye, center, up);

        modelMatrix.mul(worldToCamMat);

        glUseProgram(program.theProgram);
        glUniformMatrix4fv(program.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

        glActiveTexture(GL_TEXTURE0 + colorTexUnit);
        glBindTexture(GL_TEXTURE_2D, useMipmapTexture ? mipmapTestTexture : checkerTexture);
        glBindSampler(colorTexUnit, samplers[currSampler]);

        if (drawCorridor) {
            corridor.render("tex");
        } else {
            plane.render("tex");
        }

        glBindSampler(colorTexUnit, 0);
        glBindTexture(GL_TEXTURE_2D, 0);

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(90.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData program;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        program = loadProgram("PT.vert", "Tex.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int colorTextureUnif = glGetUniformLocation(data.theProgram, "colorTexture");
        glUseProgram(data.theProgram);
        glUniform1i(colorTextureUnif, colorTexUnit);
        glUseProgram(0);

        return data;
    }

    ////////////////////////////////
    private Mesh plane;
    private Mesh corridor;

    private final int NUM_SAMPLERS = 6;
    private int[] samplers = new int[NUM_SAMPLERS];
    private int currSampler;

    private final int colorTexUnit = 0;

    private int checkerTexture;
    private int mipmapTestTexture;

    private final byte[] mipmapColors = {
            (byte) 0xFF, (byte) 0xFF, 0x00,
            (byte) 0xFF, 0x00, (byte) 0xFF,
            0x00, (byte) 0xFF, (byte) 0xFF,
            (byte) 0xFF, 0x00, 0x00,
            0x00, (byte) 0xFF, 0x00,
            0x00, 0x00, (byte) 0xFF,
            0x00, 0x00, 0x00,
            (byte) 0xFF, (byte) 0xFF, (byte) 0xFF
    };

    private final String[] samplerNames = {
            "Nearest",
            "Linear",
            "Linear with nearest mipmaps",
            "Linear with linear mipmaps",
            "Low anisotropic",
            "Max anisotropic"
    };

    private Timer camTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean useMipmapTexture;
    private boolean drawCorridor;


    private void loadMipmapTexture() {
        mipmapTestTexture = glGenTextures();
        glBindTexture(GL_TEXTURE_2D, mipmapTestTexture);

        int oldAlign = glGetInteger(GL_UNPACK_ALIGNMENT);
        glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

        for (int mipmapLevel = 0; mipmapLevel < 8; mipmapLevel++) {
            int width = 128 >> mipmapLevel;
            int height = 128 >> mipmapLevel;
            ArrayList<Byte> texture = new ArrayList<>();

            final int currColor = mipmapLevel * 3;
            fillWithColor(texture, mipmapColors[currColor], mipmapColors[currColor + 1], mipmapColors[currColor + 2], width, height);

            ByteBuffer textureBuffer = BufferUtils.createByteBuffer(texture.size());
            for (Byte b : texture) {
                textureBuffer.put(b);
            }
            textureBuffer.flip();

            glTexImage2D(GL_TEXTURE_2D, mipmapLevel, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, textureBuffer);
        }

        glPixelStorei(GL_UNPACK_ALIGNMENT, oldAlign);

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 7);
        glBindTexture(GL_TEXTURE_2D, 0);
    }

    private void fillWithColor(ArrayList<Byte> buffer, byte red, byte green, byte blue, int width, int height) {
        int numTexels = width * height;
        for (int i = 0; i < numTexels * 3; i++) {
            buffer.add(red);
            buffer.add(green);
            buffer.add(blue);
        }
    }


    private void loadCheckerTexture() {
        try {
            String filePath = Framework.findFileOrThrow("checker.dds");
            ImageSet imageSet = DdsLoader.loadFromFile(filePath);

            checkerTexture = glGenTextures();
            glBindTexture(GL_TEXTURE_2D, checkerTexture);

            for (int mipmapLevel = 0; mipmapLevel < imageSet.getMipmapCount(); mipmapLevel++) {
                ImageSet.SingleImage image = imageSet.getImage(mipmapLevel, 0, 0);
                ImageSet.Dimensions imageDimensions = image.getDimensions();

                glTexImage2D(GL_TEXTURE_2D, mipmapLevel, GL_RGB8, imageDimensions.width, imageDimensions.height, 0,
                        GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, image.getImageData());
            }

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);
            glBindTexture(GL_TEXTURE_2D, 0);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }


    private void createSamplers() {
        for (int samplerIndex = 0; samplerIndex < NUM_SAMPLERS; samplerIndex++) {
            samplers[samplerIndex] = glGenSamplers();
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_S, GL_REPEAT);
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_T, GL_REPEAT);
        }

        // Nearest
        glSamplerParameteri(samplers[0], GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glSamplerParameteri(samplers[0], GL_TEXTURE_MIN_FILTER, GL_NEAREST);

        // Linear
        glSamplerParameteri(samplers[1], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[1], GL_TEXTURE_MIN_FILTER, GL_LINEAR);

        // Linear mipmap Nearest
        glSamplerParameteri(samplers[2], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[2], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);

        // Linear mipmap linear
        glSamplerParameteri(samplers[3], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[3], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

        // Low anisotropic
        glSamplerParameteri(samplers[4], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[4], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glSamplerParameterf(samplers[4], GL_TEXTURE_MAX_ANISOTROPY_EXT, 4.0f);

        // Max anisotropic
        float maxAniso = glGetFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT);
        System.out.printf("Maximum anisotropy: %f\n", maxAniso);

        glSamplerParameteri(samplers[5], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[5], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glSamplerParameterf(samplers[5], GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso);
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;
} public class GammaCheckers extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut16/data/";
        new GammaCheckers().start(500, 500);
    }


    @Override
    protected void init() {
        initializePrograms();

        corridor = new Mesh("Corridor.xml");
        plane = new Mesh("BigPlane.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        loadCheckerTexture();
        createSamplers();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                boolean particularKeyPressed = false;
                switch (key) {
                    case GLFW_KEY_A:
                        drawGammaProgram = !drawGammaProgram;
                        particularKeyPressed = true;
                        break;

                    case GLFW_KEY_G:
                        drawGammaTexture = !drawGammaTexture;
                        particularKeyPressed = true;
                        break;

                    case GLFW_KEY_SPACE:
                        drawGammaProgram = !drawGammaProgram;
                        drawGammaTexture = !drawGammaTexture;
                        particularKeyPressed = true;
                        break;

                    case GLFW_KEY_Y:
                        drawCorridor = !drawCorridor;
                        break;

                    case GLFW_KEY_P:
                        camTimer.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }

                if (particularKeyPressed) {
                    System.out.printf("----\n");
                    System.out.printf("Rendering:\t\t%s\n", drawGammaProgram ? "Gamma" : "Linear");
                    System.out.printf("Mipmap Generation:\t%s\n", drawGammaTexture ? "Gamma" : "Linear");
                }

                if (GLFW_KEY_1 <= key && key <= GLFW_KEY_9) {
                    int number = key - GLFW_KEY_1;
                    if (number < NUM_SAMPLERS) {
                        currSampler = number;
                    }
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.75f, 0.75f, 1.0f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        camTimer.update(elapsedTime);

        float cyclicAngle = camTimer.getAlpha() * 6.28f;
        float hOffset = (float) (Math.cos(cyclicAngle) * 0.25f);
        float vOffset = (float) (Math.sin(cyclicAngle) * 0.25f);

        Vector3f eye = new Vector3f(hOffset, 1.0f, -64.0f);
        Vector3f center = new Vector3f(hOffset, -5.0f + vOffset, -44.0f);
        Vector3f up = new Vector3f(0.0f, 1.0f, 0.0f);
        final Matrix4f worldToCamMat = new Matrix4f().lookAt(eye, center, up);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(worldToCamMat);

        final ProgramData prog = drawGammaProgram ? progGamma : progNoGamma;

        glUseProgram(prog.theProgram);
        glUniformMatrix4fv(prog.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

        glActiveTexture(GL_TEXTURE0 + colorTexUnit);
        glBindTexture(GL_TEXTURE_2D, drawGammaTexture ? gammaTexture : linearTexture);
        glBindSampler(colorTexUnit, samplers[currSampler]);

        if (drawCorridor) {
            corridor.render("tex");
        } else {
            plane.render("tex");
        }

        glBindSampler(colorTexUnit, 0);
        glBindTexture(GL_TEXTURE_2D, 0);

        glUseProgram(0);
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(90.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private ProgramData progNoGamma;
    private ProgramData progGamma;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;
    }


    private void initializePrograms() {
        progNoGamma = loadProgram("PT.vert", "textureNoGamma.frag");
        progGamma = loadProgram("PT.vert", "textureGamma.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int colorTextureUnif = glGetUniformLocation(data.theProgram, "colorTexture");
        glUseProgram(data.theProgram);
        glUniform1i(colorTextureUnif, colorTexUnit);
        glUseProgram(0);

        return data;
    }

    ////////////////////////////////
    private Mesh plane;
    private Mesh corridor;

    private final int colorTexUnit = 0;

    private final int NUM_SAMPLERS = 2;
    private int[] samplers = new int[NUM_SAMPLERS];
    private int currSampler;

    private int linearTexture;
    private int gammaTexture;

    private Timer camTimer = new Timer(Timer.Type.LOOP, 5.0f);

    private boolean drawCorridor;
    private boolean drawGammaTexture;
    private boolean drawGammaProgram;


    private void loadCheckerTexture() {
        try {
            String filePath = Framework.findFileOrThrow("checker_linear.dds");
            ImageSet imageSet = DdsLoader.loadFromFile(filePath);

            linearTexture = glGenTextures();
            glBindTexture(GL_TEXTURE_2D, linearTexture);

            for (int mipmapLevel = 0; mipmapLevel < imageSet.getMipmapCount(); mipmapLevel++) {
                SingleImage image = imageSet.getImage(mipmapLevel, 0, 0);
                Dimensions imageDimensions = image.getDimensions();

                glTexImage2D(GL_TEXTURE_2D, mipmapLevel, GL_SRGB8, imageDimensions.width, imageDimensions.height, 0,
                        GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, image.getImageData());
            }

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);


            filePath = Framework.findFileOrThrow("checker_gamma.dds");
            imageSet = DdsLoader.loadFromFile(filePath);

            gammaTexture = glGenTextures();
            glBindTexture(GL_TEXTURE_2D, gammaTexture);

            for (int mipmapLevel = 0; mipmapLevel < imageSet.getMipmapCount(); mipmapLevel++) {
                SingleImage image = imageSet.getImage(mipmapLevel, 0, 0);
                Dimensions imageDimensions = image.getDimensions();

                glTexImage2D(GL_TEXTURE_2D, mipmapLevel, GL_SRGB8, imageDimensions.width, imageDimensions.height, 0,
                        GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV, image.getImageData());
            }

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);

            glBindTexture(GL_TEXTURE_2D, 0);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }


    private void createSamplers() {
        for (int samplerIndex = 0; samplerIndex < NUM_SAMPLERS; samplerIndex++) {
            samplers[samplerIndex] = glGenSamplers();
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_S, GL_REPEAT);
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_T, GL_REPEAT);
        }

        // Linear mipmap linear
        glSamplerParameteri(samplers[0], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[0], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

        // Max anisotropic
        float maxAniso = glGetFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT);

        glSamplerParameteri(samplers[1], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[1], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glSamplerParameterf(samplers[1], GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso);
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;
} public class GammaLandscape extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut16/data/";
        new GammaLandscape().start(700, 700);
    }


    @Override
    protected void init() {
        lightEnv = new LightEnv("LightEnv.xml");

        initializePrograms();

        terrain = new Mesh("terrain.xml");
        sphere = new Mesh("UnitSphere.xml");

        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        loadTextures();
        createSamplers();


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        useGammaDisplay = !useGammaDisplay;
                        break;

                    case GLFW_KEY_MINUS:
                        lightEnv.rewindTime(1.0f);
                        break;

                    case GLFW_KEY_EQUAL:
                        lightEnv.fastForwardTime(1.0f);
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_P:
                        lightEnv.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }

                if (GLFW_KEY_1 <= key && key <= GLFW_KEY_9) {
                    int number = key - GLFW_KEY_1;
                    if (number < NUM_SAMPLERS) {
                        currSampler = number;
                    }
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        if (useGammaDisplay) {
            glEnable(GL_FRAMEBUFFER_SRGB);
        } else {
            glDisable(GL_FRAMEBUFFER_SRGB);
        }

        lightEnv.updateTime(elapsedTime);

        Vector4f bgColor = lightEnv.getBackgroundColor();
        glClearColor(bgColor.x, bgColor.y, bgColor.z, bgColor.w);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        LightBlock lightData = lightEnv.getLightBlock(viewPole.calcMatrix());

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, lightData.getAndFlip(lightBlockBuffer), GL_STREAM_DRAW);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        modelMatrix.pushMatrix();
        modelMatrix.rotateX((float) Math.toRadians(-90.0f));

        glUseProgram(progStandard.theProgram);
        glUniformMatrix4fv(progStandard.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
        glUniform1i(progStandard.numberOfLightsUnif, lightEnv.getNumLights());

        glActiveTexture(GL_TEXTURE0 + colorTexUnit);
        glBindTexture(GL_TEXTURE_2D, linearTexture);
        glBindSampler(colorTexUnit, samplers[currSampler]);

        terrain.render("lit-tex");

        glBindSampler(colorTexUnit, 0);
        glBindTexture(GL_TEXTURE_2D, 0);

        glUseProgram(0);

        modelMatrix.popMatrix();

        // Render the sun
        {
            modelMatrix.pushMatrix();

            Vector4f tmp = lightEnv.getSunlightDirection();
            Vector3f sunlightDir = new Vector3f(tmp.x, tmp.y, tmp.z);
            modelMatrix.translate(sunlightDir.mul(500.0f));
            modelMatrix.scale(30.0f, 30.0f, 30.0f);

            glUseProgram(progUnlit.theProgram);
            glUniformMatrix4fv(progUnlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = lightEnv.getSunlightScaledIntensity();
            glUniform4fv(progUnlit.objectColorUnif, lightColor.get(vec4Buffer));
            sphere.render("flat");

            modelMatrix.popMatrix();
        }

        // Draw lights
        for (int light = 0; light < lightEnv.getNumPointLights(); light++) {
            modelMatrix.pushMatrix();

            modelMatrix.translate(lightEnv.getPointLightWorldPos(light));

            glUseProgram(progUnlit.theProgram);
            glUniformMatrix4fv(progUnlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));

            Vector4f lightColor = lightEnv.getPointLightScaledIntensity(light);
            glUniform4fv(progUnlit.objectColorUnif, lightColor.get(vec4Buffer));
            sphere.render("flat");

            modelMatrix.popMatrix();
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            // Draw lookat point.
            modelMatrix.identity();
            modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(progUnlit.theProgram);
            glUniformMatrix4fv(progUnlit.modelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(progUnlit.objectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            sphere.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(progUnlit.objectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            sphere.render("flat");

            modelMatrix.popMatrix();
        }
    }

    @Override
    protected void reshape(int w, int h) {
        float zNear = 1.0f;
        float zFar = 1000.0f;
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.perspective((float) Math.toRadians(60.0f), (w / (float) h), zNear, zFar);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
        final float scale = 20;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private ProgramData progStandard;
    private UnlitProgData progUnlit;

    private class ProgramData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int numberOfLightsUnif;
    }

    private class UnlitProgData {
        int theProgram;

        int modelToCameraMatrixUnif;
        int objectColorUnif;
    }


    private void initializePrograms() {
        progStandard = loadProgram("PNT.vert", "litTexture.frag");
        progUnlit = loadUnlitProgram("Unlit.vert", "Unlit.frag");
    }

    private ProgramData loadProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        ProgramData data = new ProgramData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.numberOfLightsUnif = glGetUniformLocation(data.theProgram, "numberOfLights");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        int lightBlock = glGetUniformBlockIndex(data.theProgram, "Light");
        glUniformBlockBinding(data.theProgram, lightBlock, lightBlockIndex);

        int colorTextureUnif = glGetUniformLocation(data.theProgram, "diffuseColorTex");
        glUseProgram(data.theProgram);
        glUniform1i(colorTextureUnif, colorTexUnit);
        glUseProgram(0);

        return data;
    }

    private UnlitProgData loadUnlitProgram(String vertexShaderFileName, String fragmentShaderFileName) {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, vertexShaderFileName));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, fragmentShaderFileName));

        UnlitProgData data = new UnlitProgData();
        data.theProgram = Framework.createProgram(shaderList);
        data.modelToCameraMatrixUnif = glGetUniformLocation(data.theProgram, "modelToCameraMatrix");
        data.objectColorUnif = glGetUniformLocation(data.theProgram, "objectColor");

        int projectionBlock = glGetUniformBlockIndex(data.theProgram, "Projection");
        glUniformBlockBinding(data.theProgram, projectionBlock, projectionBlockIndex);

        return data;
    }

    ////////////////////////////////
    private Mesh terrain;
    private Mesh sphere;

    private final int lightBlockIndex = 1;
    private final int colorTexUnit = 0;

    private final int NUM_SAMPLERS = 2;
    private int[] samplers = new int[NUM_SAMPLERS];
    private int currSampler;

    private int lightUniformBuffer;
    private int linearTexture;

    private LightEnv lightEnv;

    private boolean useGammaDisplay = true;
    private boolean drawCameraPos;


    private void loadTextures() {
        try {
            String filePath = Framework.findFileOrThrow("terrain_tex.dds");
            ImageSet imageSet = DdsLoader.loadFromFile(filePath);

            linearTexture = glGenTextures();
            glBindTexture(GL_TEXTURE_2D, linearTexture);

            TextureGenerator.OpenGLPixelTransferParams xfer = TextureGenerator.getUploadFormatType(imageSet.getFormat(), 0);

            for (int mipmapLevel = 0; mipmapLevel < imageSet.getMipmapCount(); mipmapLevel++) {
                ImageSet.SingleImage image = imageSet.getImage(mipmapLevel, 0, 0);
                ImageSet.Dimensions imageDimensions = image.getDimensions();

                glTexImage2D(GL_TEXTURE_2D, mipmapLevel, GL_SRGB8_ALPHA8, imageDimensions.width,
                        imageDimensions.height, 0, xfer.format, xfer.type, image.getImageData());
            }

            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);

            glBindTexture(GL_TEXTURE_2D, 0);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }
    }


    private void createSamplers() {
        for (int samplerIndex = 0; samplerIndex < NUM_SAMPLERS; samplerIndex++) {
            samplers[samplerIndex] = glGenSamplers();
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_S, GL_REPEAT);
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_WRAP_T, GL_REPEAT);
        }

        // Linear mipmap linear
        glSamplerParameteri(samplers[0], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[0], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);

        // Max anisotropic
        float maxAniso = glGetFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT);

        glSamplerParameteri(samplers[1], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glSamplerParameteri(samplers[1], GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
        glSamplerParameterf(samplers[1], GL_TEXTURE_MAX_ANISOTROPY_EXT, maxAniso);
    }

    ////////////////////////////////
    // View setup.
    private ViewData initialView = new ViewData(
            new Vector3f(-60.257084f, 10.947238f, 62.636356f),
            new Quaternionf(-0.099283f, -0.211198f, -0.020028f, -0.972817f),
            30.0f,
            0.0f
    );

    private ViewScale initialViewScale = new ViewScale(
            5.0f, 90.0f,
            2.0f, 0.5f,
            4.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialView, initialViewScale, MouseButtons.MB_LEFT_BTN);

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;
} public class GammaRamp extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut16/data/";
        new GammaRamp().start(500, 195);
    }


    @Override
    protected void init() {
        initializeProgram();
        initializeVertexData();
        loadTextures();

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_DYNAMIC_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_1:
                        useGammaCorrect[0] = !useGammaCorrect[0];
                        if (useGammaCorrect[0]) {
                            System.out.printf("Top:\tsRGB texture.\n");
                        } else {
                            System.out.printf("Top:\tlinear texture.\n");
                        }
                        break;

                    case GLFW_KEY_2:
                        useGammaCorrect[1] = !useGammaCorrect[1];
                        if (useGammaCorrect[1]) {
                            System.out.printf("Bottom:\tsRGB texture.\n");
                        } else {
                            System.out.printf("Bottom:\tlinear texture.\n");
                        }
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });
    }

    @Override
    protected void display() {
        glClearColor(0.0f, 0.5f, 0.3f, 0.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        glActiveTexture(GL_TEXTURE0 + gammaRampTextureUnit);
        glBindTexture(GL_TEXTURE_2D, textures[useGammaCorrect[0] ? 1 : 0]);
        glBindSampler(gammaRampTextureUnit, samplerObj);

        glBindVertexArray(vao);

        glUseProgram(noGammaProgram);
        glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

        glBindTexture(GL_TEXTURE_2D, textures[useGammaCorrect[1] ? 1 : 0]);

        glUseProgram(gammaProgram);
        glDrawArrays(GL_TRIANGLE_STRIP, 4, 4);

        glBindVertexArray(0);
        glUseProgram(0);

        glActiveTexture(GL_TEXTURE0 + gammaRampTextureUnit);
        glBindTexture(GL_TEXTURE_2D, 0);
        glBindSampler(gammaRampTextureUnit, 0);
    }

    @Override
    protected void reshape(int w, int h) {
        Matrix4f persMatrix = new Matrix4f();
        persMatrix.translate(-1.0f, 1.0f, 0.0f);
        persMatrix.scale(2.0f / w, -2.0f / h, 1.0f);

        ProjectionBlock projData = new ProjectionBlock();
        projData.cameraToClipMatrix = persMatrix;

        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, projData.getAndFlip(projectionBlockBuffer));
        glBindBuffer(GL_UNIFORM_BUFFER, 0);

        glViewport(0, 0, w, h);
    }

    @Override
    protected void update() {
    }

    ////////////////////////////////
    private int gammaProgram;
    private int noGammaProgram;


    private void initializeProgram() {
        ArrayList<Integer> shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "screenCoords.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "textureNoGamma.frag"));
        noGammaProgram = Framework.createProgram(shaderList);

        shaderList = new ArrayList<>();
        shaderList.add(Framework.loadShader(GL_VERTEX_SHADER, "screenCoords.vert"));
        shaderList.add(Framework.loadShader(GL_FRAGMENT_SHADER, "textureGamma.frag"));
        gammaProgram = Framework.createProgram(shaderList);

        int projectionBlock = glGetUniformBlockIndex(noGammaProgram, "Projection");
        glUniformBlockBinding(noGammaProgram, projectionBlock, projectionBlockIndex);

        int colorTextureUnif = glGetUniformLocation(noGammaProgram, "colorTexture");
        glUseProgram(noGammaProgram);
        glUniform1i(colorTextureUnif, gammaRampTextureUnit);
        glUseProgram(0);

        projectionBlock = glGetUniformBlockIndex(gammaProgram, "Projection");
        glUniformBlockBinding(gammaProgram, projectionBlock, projectionBlockIndex);

        colorTextureUnif = glGetUniformLocation(gammaProgram, "colorTexture");
        glUseProgram(gammaProgram);
        glUniform1i(colorTextureUnif, gammaRampTextureUnit);
        glUseProgram(0);
    }

    ////////////////////////////////
    private final short[] vertexData = {
            90, 80, 0, 0,
            90, 16, 0, (short) 65535,
            410, 80, (short) 65535, 0,
            410, 16, (short) 65535, (short) 65535,

            90, 176, 0, 0,
            90, 112, 0, (short) 65535,
            410, 176, (short) 65535, 0,
            410, 112, (short) 65535, (short) 65535
    };

    private int vao;


    private void initializeVertexData() {
        int dataBufferObject = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, dataBufferObject);

        ShortBuffer vertexDataBuffer = BufferUtils.createShortBuffer(vertexData.length);
        vertexDataBuffer.put(vertexData);
        vertexDataBuffer.flip();

        glBufferData(GL_ARRAY_BUFFER, vertexDataBuffer, GL_STATIC_DRAW);

        vao = glGenVertexArrays();

        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, dataBufferObject);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2, GL_UNSIGNED_SHORT, false, 8, 0);
        glEnableVertexAttribArray(5);
        glVertexAttribPointer(5, 2, GL_UNSIGNED_SHORT, true, 8, 4);

        glBindVertexArray(0);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
    }

    ////////////////////////////////
    private final int gammaRampTextureUnit = 0;

    private int[] textures = new int[2];

    private int samplerObj;

    private boolean[] useGammaCorrect = {false, false};


    private void loadTextures() {
        textures[0] = glGenTextures();
        textures[1] = glGenTextures();

        try {
            String filePath = Framework.findFileOrThrow("gamma_ramp.png");
            ImageSet imageSet = StbLoader.loadFromFile(filePath);

            ImageSet.SingleImage image = imageSet.getImage(0, 0, 0);
            ImageSet.Dimensions imageDimensions = image.getDimensions();

            TextureGenerator.OpenGLPixelTransferParams pxTrans = TextureGenerator.getUploadFormatType(imageSet.getFormat(), 0);

            glBindTexture(GL_TEXTURE_2D, textures[0]);

            glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, imageDimensions.width, imageDimensions.height, 0,
                    pxTrans.format, pxTrans.type, image.getImageData());
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);

            glBindTexture(GL_TEXTURE_2D, textures[1]);
            glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB8, imageDimensions.width, imageDimensions.height, 0,
                    pxTrans.format, pxTrans.type, image.getImageData());
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, imageSet.getMipmapCount() - 1);

            glBindTexture(GL_TEXTURE_2D, 0);
        } catch (Exception e) {
            e.printStackTrace();
            System.exit(-1);
        }

        samplerObj = glGenSamplers();
        glSamplerParameteri(samplerObj, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(samplerObj, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(samplerObj, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glSamplerParameteri(samplerObj, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;
} public class CubePointLight extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut17/data/";
        new CubePointLight().start(displayWidth, displayHeight);
    }


    @Override
    protected void init() {
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);
        glEnable(GL_FRAMEBUFFER_SRGB);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        createSamplers();
        loadTextures();

        loadAndSetupScene();

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        GL15.glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        lightPole.reset();
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        showOtherLights = !showOtherLights;
                        break;

                    case GLFW_KEY_P:
                        timer.togglePause();
                        break;

                    case GLFW_KEY_1:
                    case GLFW_KEY_2:
                        currTextureIndex = key - GLFW_KEY_1;
                        System.out.printf("%s\n", texDefs[currTextureIndex].name);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, lightPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(lightPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, lightPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        timer.update(elapsedTime);

        glClearColor(0.8f, 0.8f, 0.8f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        final Matrix4f cameraMatrix = viewPole.calcMatrix();
        final Matrix4f lightView = lightPole.calcMatrix();

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(cameraMatrix);

        buildLights(cameraMatrix);

        float angle0 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation0 = new Quaternionf().rotateAxis(angle0, 0.0f, 1.0f, 0.0f);
        nodes.get(0).nodeSetOrient(rotation0);

        float angle3 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation3 = new Quaternionf().rotateAxis(angle3, 0.0f, 0.0f, 1.0f);
        nodes.get(3).nodeSetOrient(new Quaternionf(spinBarOrient).mul(rotation3));

        {
            final float zNear = 1.0f;
            final float zFar = 1000.0f;
            MatrixStackf persMatrix = new MatrixStackf();
            persMatrix.perspective((float) Math.toRadians(60.0f), displayWidth / (float) displayHeight, zNear, zFar);

            ProjectionBlock projData = new ProjectionBlock();
            projData.cameraToClipMatrix = persMatrix;

            glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
            glBufferData(GL_UNIFORM_BUFFER, projData.getAndFlip(projectionBlockBuffer), GL_STREAM_DRAW);
            glBindBuffer(GL_UNIFORM_BUFFER, 0);
        }

        glActiveTexture(GL_TEXTURE0 + lightProjTexUnit);
        glBindTexture(GL_TEXTURE_CUBE_MAP, lightTextures[currTextureIndex]);
        glBindSampler(lightProjTexUnit, samplers[currSampler]);

        {
            MatrixStackf lightProjStack = new MatrixStackf();
            lightProjStack.mul(new Matrix4f(lightView).invert());
            lightProjStack.mul(new Matrix4f(cameraMatrix).invert());
            lightProjMatBinder.setValue(lightProjStack);

            Vector4f worldLightPos = new Vector4f(lightView.m30(), lightView.m31(), lightView.m32(), lightView.m33());
            Vector4f tmp = cameraMatrix.transform(new Vector4f(worldLightPos));
            Vector3f lightPos = new Vector3f(tmp.x, tmp.y, tmp.z);
            camLightPosBinder.setValue(lightPos);
        }

        glViewport(0, 0, displayWidth, displayHeight);
        scene.render(modelMatrix);

        {
            // Draw axes
            modelMatrix.pushMatrix();

            modelMatrix.mul(lightView);
            modelMatrix.scale(15.0f);

            glUseProgram(coloredProg);
            glUniformMatrix4fv(coloredModelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            axesMesh.render();

            modelMatrix.popMatrix();
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            // Draw lookat point.
            modelMatrix.identity();
            modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);
            modelMatrix.scale(0.5f);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlitProg);
            glUniformMatrix4fv(unlitModelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlitObjectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            sphereMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlitObjectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            sphereMesh.render("flat");

            modelMatrix.popMatrix();
        }

        glActiveTexture(GL_TEXTURE0 + lightProjTexUnit);
        glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
        glBindSampler(lightProjTexUnit, 0);
    }

    @Override
    protected void reshape(int w, int h) {
        displayWidth = w;
        displayHeight = h;
    }

    @Override
    protected void update() {
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private int coloredModelToCameraMatrixUnif;
    private int coloredProg;

    private int unlitProg;
    private int unlitModelToCameraMatrixUnif;
    private int unlitObjectColorUnif;


    private void loadAndSetupScene() {
        scene = new Scene("projCube_scene.xml");

        nodes = new ArrayList<>();
        nodes.add(scene.findNode("cube"));
        nodes.add(scene.findNode("rightBar"));
        nodes.add(scene.findNode("leaningBar"));
        nodes.add(scene.findNode("spinBar"));
        nodes.add(scene.findNode("diorama"));
        nodes.add(scene.findNode("floor"));

        lightNumBinder = new SceneBinders.UniformIntBinder();
        SceneBinders.associateUniformWithNodes(nodes, lightNumBinder, "numberOfLights");
        SceneBinders.setStateBinderWithNodes(nodes, lightNumBinder);

        lightProjMatBinder = new SceneBinders.UniformMat4Binder();
        SceneBinders.associateUniformWithNodes(nodes, lightProjMatBinder, "cameraToLightProjMatrix");
        SceneBinders.setStateBinderWithNodes(nodes, lightProjMatBinder);

        camLightPosBinder = new SceneBinders.UniformVec3Binder();
        SceneBinders.associateUniformWithNodes(nodes, camLightPosBinder, "cameraSpaceProjLightPos");
        SceneBinders.setStateBinderWithNodes(nodes, camLightPosBinder);

        int unlit = scene.findProgram("p_unlit");
        sphereMesh = scene.findMesh("m_sphere");

        int colored = scene.findProgram("p_colored");
        axesMesh = scene.findMesh("m_axes");

        // No more things that can throw.
        spinBarOrient = nodes.get(3).nodeGetOrient();
        unlitProg = unlit;
        unlitModelToCameraMatrixUnif = glGetUniformLocation(unlit, "modelToCameraMatrix");
        unlitObjectColorUnif = glGetUniformLocation(unlit, "objectColor");

        coloredProg = colored;
        coloredModelToCameraMatrixUnif = glGetUniformLocation(colored, "modelToCameraMatrix");
    }

    ////////////////////////////////
    private static int displayWidth = 500;
    private static int displayHeight = 500;

    private Scene scene;
    private ArrayList<Scene.SceneNode> nodes;
    private Mesh sphereMesh, axesMesh;

    private SceneBinders.UniformMat4Binder lightProjMatBinder;
    private SceneBinders.UniformVec3Binder camLightPosBinder;

    private Timer timer = new Timer(Timer.Type.LOOP, 10.0f);

    private Quaternionf spinBarOrient;

    private boolean showOtherLights = true;
    private boolean drawCameraPos;

    ////////////////////////////////
    // View setup.
    private ViewData initialView = new ViewData(
            new Vector3f(0.0f, 0.0f, 10.0f),
            new Quaternionf(0.16043f, -0.376867f, -0.0664516f, 0.909845f),
            25.0f,
            0.0f
    );

    private ViewScale initialViewScale = new ViewScale(
            5.0f, 70.0f,
            2.0f, 0.5f,
            2.0f, 0.5f,
            90.0f / 250.0f
    );


    private ObjectData initLightData = new ObjectData(
            new Vector3f(0.0f, 0.0f, 10.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f)
    );


    private ViewPole viewPole = new ViewPole(initialView, initialViewScale, MouseButtons.MB_LEFT_BTN);
    private ObjectPole lightPole = new ObjectPole(initLightData, 90.0f / 250.0f, MouseButtons.MB_RIGHT_BTN, viewPole);

    ////////////////////////////////
    private final TexDef[] texDefs = {
            new TexDef("IrregularPoint.dds", "Irregular Point Light"),
            new TexDef("Planetarium.dds", "Planetarium")
    };
    private final int NUM_LIGHT_TEXTURES = texDefs.length;
    private int[] lightTextures = new int[texDefs.length];
    private int currTextureIndex = 0;

    private class TexDef {
        String filename;
        String name;

        TexDef(String filename, String name) {
            this.filename = filename;
            this.name = name;
        }
    }


    private void loadTextures() {
        try {
            for (int textureIndex = 0; textureIndex < NUM_LIGHT_TEXTURES; textureIndex++) {
                lightTextures[textureIndex] = glGenTextures();

                String filePath = Framework.findFileOrThrow(texDefs[textureIndex].filename);
                ImageSet imageSet = DdsLoader.loadFromFile(filePath);

                glBindTexture(GL_TEXTURE_CUBE_MAP, lightTextures[textureIndex]);
                glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_BASE_LEVEL, 0);
                glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAX_LEVEL, 0);

                ImageSet.Dimensions imageDimensions = imageSet.getDimensions();
                int imageFormat = TextureGenerator.getInternalFormat(imageSet.getFormat(), 0);

                for (int face = 0; face < 6; ++face) {
                    ImageSet.SingleImage singleImage = imageSet.getImage(0, 0, face);

                    glCompressedTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, imageFormat,
                            imageDimensions.width, imageDimensions.height, 0, singleImage.getImageData());
                }

                glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    ////////////////////////////////
    private final int NUM_SAMPLERS = 1;

    private int[] samplers = new int[NUM_SAMPLERS];
    private int currSampler = 0;


    private void createSamplers() {
        for (int samplerIndex = 0; samplerIndex < NUM_SAMPLERS; samplerIndex++) {
            samplers[samplerIndex] = glGenSamplers();
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        }

        glSamplerParameteri(samplers[0], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(samplers[0], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(samplers[0], GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;
    private final int lightProjTexUnit = 3;

    private int lightUniformBuffer;
    private SceneBinders.UniformIntBinder lightNumBinder;


    private void buildLights(Matrix4f camMatrix) {
        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        lightData.lightAttenuation = 1.0f / (30.0f * 30.0f);
        lightData.maxIntensity = 2.0f;

        lightData.lights[0] = new PerLight();
        lightData.lights[0].lightIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        lightData.lights[0].cameraSpaceLightPos = camMatrix.transform(new Vector4f(-0.2f, 0.5f, 0.5f, 0.0f).normalize());

        lightData.lights[1] = new PerLight();
        lightData.lights[1].lightIntensity = new Vector4f(3.5f, 6.5f, 3.0f, 1.0f).mul(0.5f);
        lightData.lights[1].cameraSpaceLightPos = camMatrix.transform(new Vector4f(5.0f, 6.0f, 0.5f, 1.0f));

        if (showOtherLights) {
            lightNumBinder.setValue(2);
        } else {
            lightNumBinder.setValue(0);
        }

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, lightData.getAndFlip(lightBlockBuffer), GL_STREAM_DRAW);
    }
} public class DoubleProjection extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut17/data/";
        new DoubleProjection().start(displayWidth, displayHeight);
    }


    @Override
    protected void init() {
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);
        glEnable(GL_FRAMEBUFFER_SRGB);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        loadAndSetupScene();

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        persViewPole.reset();
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_Y:
                        depthClampProj = !depthClampProj;
                        break;

                    case GLFW_KEY_P:
                        timer.togglePause();
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, persViewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(persViewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, persViewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        timer.update(elapsedTime);

        glClearColor(0.8f, 0.8f, 0.8f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(viewPole.calcMatrix());

        buildLights(modelMatrix);

        float angle0 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation0 = new Quaternionf().rotateAxis(angle0, 0.0f, 1.0f, 0.0f);
        nodes.get(0).nodeSetOrient(rotation0);

        float angle3 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation3 = new Quaternionf().rotateAxis(angle3, 0.0f, 0.0f, 1.0f);
        nodes.get(3).nodeSetOrient(new Quaternionf(spinBarOrient).mul(rotation3));

        Vector2i displaySize = new Vector2i(displayWidth / 2, displayHeight);

        {
            final float zNear = 1.0f;
            final float zFar = 1000.0f;
            MatrixStackf persMatrix = new MatrixStackf();
            persMatrix.perspective((float) Math.toRadians(60.0f), displaySize.x / displaySize.y, zNear, zFar);

            ProjectionBlock projData = new ProjectionBlock();
            projData.cameraToClipMatrix = persMatrix;

            glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
            glBufferData(GL_UNIFORM_BUFFER, projData.getAndFlip(projectionBlockBuffer), GL_STREAM_DRAW);
            glBindBuffer(GL_UNIFORM_BUFFER, 0);
        }

        glViewport(0, 0, displaySize.x, displaySize.y);
        scene.render(modelMatrix);

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            // Draw lookat point.
            modelMatrix.identity();
            modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);
            modelMatrix.scale(0.5f);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlitProg);
            glUniformMatrix4fv(unlitModelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlitObjectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            sphereMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlitObjectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            sphereMesh.render("flat");

            modelMatrix.popMatrix();
        }

        {
            final float zNear = 1.0f;
            final float zFar = 1000.0f;
            MatrixStackf persMatrix = new MatrixStackf();
            persMatrix.mul(new Matrix4f(new Matrix3f(persViewPole.calcMatrix())));
            persMatrix.perspective((float) Math.toRadians(60.0f), displaySize.x / displaySize.y, zNear, zFar);

            ProjectionBlock projData = new ProjectionBlock();
            projData.cameraToClipMatrix = persMatrix;

            glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
            glBufferData(GL_UNIFORM_BUFFER, projData.getAndFlip(projectionBlockBuffer), GL_STREAM_DRAW);
            glBindBuffer(GL_UNIFORM_BUFFER, 0);
        }

        if (!depthClampProj) {
            glDisable(GL_DEPTH_CLAMP);
        }

        glViewport(displaySize.x + (displayWidth % 2), 0, displaySize.x, displaySize.y);
        scene.render(modelMatrix);
        glEnable(GL_DEPTH_CLAMP);
    }

    @Override
    protected void reshape(int w, int h) {
        displayWidth = w;
        displayHeight = h;
    }

    @Override
    protected void update() {
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private int unlitProg;
    private int unlitModelToCameraMatrixUnif;
    private int unlitObjectColorUnif;


    private void loadAndSetupScene() {
        scene = new Scene("dp_scene.xml");

        nodes = new ArrayList<>();
        nodes.add(scene.findNode("cube"));
        nodes.add(scene.findNode("rightBar"));
        nodes.add(scene.findNode("leaningBar"));
        nodes.add(scene.findNode("spinBar"));

        lightNumBinder = new SceneBinders.UniformIntBinder();
        SceneBinders.associateUniformWithNodes(nodes, lightNumBinder, "numberOfLights");
        SceneBinders.setStateBinderWithNodes(nodes, lightNumBinder);

        int unlit = scene.findProgram("p_unlit");
        sphereMesh = scene.findMesh("m_sphere");

        spinBarOrient = nodes.get(3).nodeGetOrient();
        unlitProg = unlit;
        unlitModelToCameraMatrixUnif = glGetUniformLocation(unlit, "modelToCameraMatrix");
        unlitObjectColorUnif = glGetUniformLocation(unlit, "objectColor");
    }

    ////////////////////////////////
    private static int displayWidth = 700;
    private static int displayHeight = 350;

    private Scene scene;
    private ArrayList<Scene.SceneNode> nodes;
    private Mesh sphereMesh;

    private Timer timer = new Timer(Timer.Type.LOOP, 10.0f);

    private Quaternionf spinBarOrient;

    private boolean depthClampProj = true;
    private boolean drawCameraPos;

    ////////////////////////////////
    // View setup.
    private ViewData initialView = new ViewData(
            new Vector3f(0.0f, 0.0f, 0.0f),
            new Quaternionf(0.16043f, -0.376867f, -0.0664516f, 0.909845f),
            25.0f,
            0.0f
    );

    private ViewScale initialViewScale = new ViewScale(
            5.0f, 70.0f,
            2.0f, 0.5f,
            2.0f, 0.5f,
            90.0f / 250.0f
    );


    private ViewData initPersView = new ViewData(
            new Vector3f(0.0f, 0.0f, 0.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f),
            5.0f,
            0.0f
    );

    private ViewScale initPersViewScale = new ViewScale(
            0.05f, 10.0f,
            0.1f, 0.05f,
            4.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialView, initialViewScale, MouseButtons.MB_LEFT_BTN);
    private ViewPole persViewPole = new ViewPole(initPersView, initPersViewScale, MouseButtons.MB_RIGHT_BTN);

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;

    private int lightUniformBuffer;
    private SceneBinders.UniformIntBinder lightNumBinder;


    private void buildLights(Matrix4f camMatrix) {
        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        lightData.lightAttenuation = 1.0f / (5.0f * 5.0f);
        lightData.maxIntensity = 3.0f;

        lightData.lights[0] = new PerLight();
        lightData.lights[0].lightIntensity = new Vector4f(2.0f, 2.0f, 2.5f, 1.0f);
        lightData.lights[0].cameraSpaceLightPos = camMatrix.transform(new Vector4f(-0.2f, 0.5f, 0.5f, 0.0f).normalize());

        lightData.lights[1] = new PerLight();
        lightData.lights[1].lightIntensity = new Vector4f(3.5f, 6.5f, 3.0f, 1.0f).mul(1.2f);
        lightData.lights[1].cameraSpaceLightPos = camMatrix.transform(new Vector4f(5.0f, 6.0f, 0.5f, 1.0f));

        lightNumBinder.setValue(2);

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, lightData.getAndFlip(lightBlockBuffer), GL_STREAM_DRAW);
    }
} public class ProjectedLight extends Tutorial {
    public static void main(String[] args) {
        Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut17/data/";
        new ProjectedLight().start(displayWidth, displayHeight);
    }


    @Override
    protected void init() {
        glEnable(GL_CULL_FACE);
        glCullFace(GL_BACK);
        glFrontFace(GL_CW);

        final float depthZNear = 0.0f;
        final float depthZFar = 1.0f;

        glEnable(GL_DEPTH_TEST);
        glDepthMask(true);
        glDepthFunc(GL_LEQUAL);
        glDepthRange(depthZNear, depthZFar);
        glEnable(GL_DEPTH_CLAMP);
        glEnable(GL_FRAMEBUFFER_SRGB);

        // Setup our Uniform Buffers
        projectionUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, ProjectionBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, projectionBlockIndex, projectionUniformBuffer, 0, ProjectionBlock.BYTES);

        createSamplers();
        loadTextures();

        loadAndSetupScene();

        lightUniformBuffer = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, LightBlock.BYTES, GL_STREAM_DRAW);

        glBindBufferRange(GL_UNIFORM_BUFFER, lightBlockIndex, lightUniformBuffer, 0, LightBlock.BYTES);

        glBindBuffer(GL_UNIFORM_BUFFER, 0);


        glfwSetKeyCallback(window, (window, key, scancode, action, mods) -> {
            if (action == GLFW_PRESS) {
                switch (key) {
                    case GLFW_KEY_SPACE:
                        lightViewPole.reset();
                        break;

                    case GLFW_KEY_T:
                        drawCameraPos = !drawCameraPos;
                        break;

                    case GLFW_KEY_G:
                        showOtherLights = !showOtherLights;
                        break;

                    case GLFW_KEY_H:
                        currSampler = (currSampler + 1) % NUM_SAMPLERS;
                        break;

                    case GLFW_KEY_P:
                        timer.togglePause();
                        break;

                    case GLFW_KEY_Y:
                        currFOVIndex = Math.min(currFOVIndex + 1, lightFOVs.length - 1);
                        System.out.printf("Curr FOV: %f\n", lightFOVs[currFOVIndex]);
                        break;

                    case GLFW_KEY_N:
                        currFOVIndex = Math.max(currFOVIndex - 1, 0);
                        System.out.printf("Curr FOV: %f\n", lightFOVs[currFOVIndex]);
                        break;

                    case GLFW_KEY_1:
                    case GLFW_KEY_2:
                    case GLFW_KEY_3:
                        currTextureIndex = key - GLFW_KEY_1;
                        System.out.printf("%s\n", texDefs[currTextureIndex].name);
                        break;

                    case GLFW_KEY_ESCAPE:
                        glfwSetWindowShouldClose(window, true);
                        break;
                }
            }
        });

        glfwSetMouseButtonCallback(window, (window, button, action, mods) -> {
            boolean pressed = action == GLFW_PRESS;
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseButton(window, viewPole, button, pressed, x, y);
            MousePole.forwardMouseButton(window, lightViewPole, button, pressed, x, y);
        });
        glfwSetCursorPosCallback(window, (window, xpos, ypos) -> {
            if (isMouseButtonPressed(GLFW_MOUSE_BUTTON_LEFT) || isMouseButtonPressed(GLFW_MOUSE_BUTTON_RIGHT)) {
                MousePole.forwardMouseMotion(viewPole, (int) xpos, (int) ypos);
                MousePole.forwardMouseMotion(lightViewPole, (int) xpos, (int) ypos);
            }
        });
        glfwSetScrollCallback(window, (window, xoffset, yoffset) -> {
            glfwGetCursorPos(window, mouseBuffer1, mouseBuffer2);
            int x = (int) mouseBuffer1.get(0);
            int y = (int) mouseBuffer2.get(0);
            MousePole.forwardMouseWheel(window, viewPole, (int) yoffset, x, y);
            MousePole.forwardMouseWheel(window, lightViewPole, (int) yoffset, x, y);
        });
    }

    @Override
    protected void display() {
        timer.update(elapsedTime);

        glClearColor(0.8f, 0.8f, 0.8f, 1.0f);
        glClearDepth(1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        final Matrix4f cameraMatrix = viewPole.calcMatrix();
        final Matrix4f lightView = lightViewPole.calcMatrix();

        MatrixStackf modelMatrix = new MatrixStackf(10);
        modelMatrix.mul(cameraMatrix);

        buildLights(cameraMatrix);

        float angle0 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation0 = new Quaternionf().rotateAxis(angle0, 0.0f, 1.0f, 0.0f);
        nodes.get(0).nodeSetOrient(rotation0);

        float angle3 = (float) Math.toRadians(360.0f * timer.getAlpha());
        Quaternionf rotation3 = new Quaternionf().rotateAxis(angle3, 0.0f, 0.0f, 1.0f);
        nodes.get(3).nodeSetOrient(new Quaternionf(spinBarOrient).mul(rotation3));

        {
            final float zNear = 1.0f;
            final float zFar = 1000.0f;
            MatrixStackf persMatrix = new MatrixStackf();
            persMatrix.perspective((float) Math.toRadians(60.0f), displayWidth / (float) displayHeight, zNear, zFar);

            ProjectionBlock projData = new ProjectionBlock();
            projData.cameraToClipMatrix = persMatrix;

            glBindBuffer(GL_UNIFORM_BUFFER, projectionUniformBuffer);
            glBufferData(GL_UNIFORM_BUFFER, projData.getAndFlip(projectionBlockBuffer), GL_STREAM_DRAW);
            glBindBuffer(GL_UNIFORM_BUFFER, 0);
        }

        glActiveTexture(GL_TEXTURE0 + lightProjTexUnit);
        glBindTexture(GL_TEXTURE_2D, lightTextures[currTextureIndex]);
        glBindSampler(lightProjTexUnit, samplers[currSampler]);

        {
            MatrixStackf lightProjStack = new MatrixStackf();
            // Texture-space transform
            lightProjStack.translate(0.5f, 0.5f, 0.0f);
            lightProjStack.scale(0.5f, 0.5f, 1.0f);

            // Project. Z-range is irrelevant.
            lightProjStack.perspective((float) Math.toRadians(lightFOVs[currFOVIndex]), 1.0f, 1.0f, 100.0f);

            // Transform from main camera space to light camera space.
            lightProjStack.mul(lightView);
            lightProjStack.mul(new Matrix4f(cameraMatrix).invert());
            lightProjMatBinder.setValue(lightProjStack);

            Vector4f worldLightPos = new Vector4f(lightView.m30(), lightView.m31(), lightView.m32(), lightView.m33());
            Vector4f tmp = cameraMatrix.transform(new Vector4f(worldLightPos));
            Vector3f lightPos = new Vector3f(tmp.x, tmp.y, tmp.z);
            camLightPosBinder.setValue(lightPos);
        }

        glViewport(0, 0, displayWidth, displayHeight);
        scene.render(modelMatrix);

        {
            // Draw axes
            modelMatrix.pushMatrix();

            modelMatrix.mul(new Matrix4f(lightView).invert());
            modelMatrix.scale(15.0f);
            modelMatrix.scale(1.0f, 1.0f, -1.0f);     // Invert the Z-axis so that it points in the right direction.

            glUseProgram(coloredProg);
            glUniformMatrix4fv(coloredModelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            axesMesh.render();

            modelMatrix.popMatrix();
        }

        if (drawCameraPos) {
            modelMatrix.pushMatrix();

            // Draw lookat point.
            modelMatrix.identity();
            modelMatrix.translate(0.0f, 0.0f, -viewPole.getView().radius);
            modelMatrix.scale(0.5f);

            glDisable(GL_DEPTH_TEST);
            glDepthMask(false);
            glUseProgram(unlitProg);
            glUniformMatrix4fv(unlitModelToCameraMatrixUnif, false, modelMatrix.get(mat4Buffer));
            glUniform4f(unlitObjectColorUnif, 0.25f, 0.25f, 0.25f, 1.0f);
            sphereMesh.render("flat");
            glDepthMask(true);
            glEnable(GL_DEPTH_TEST);
            glUniform4f(unlitObjectColorUnif, 1.0f, 1.0f, 1.0f, 1.0f);
            sphereMesh.render("flat");

            modelMatrix.popMatrix();
        }

        glActiveTexture(GL_TEXTURE0 + lightProjTexUnit);
        glBindTexture(GL_TEXTURE_2D, 0);
        glBindSampler(lightProjTexUnit, 0);
    }

    @Override
    protected void reshape(int w, int h) {
        displayWidth = w;
        displayHeight = h;
    }

    @Override
    protected void update() {
        final float scale = 10;

        if (isKeyPressed(GLFW_KEY_W)) {
            viewPole.charPress(GLFW_KEY_W, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_S)) {
            viewPole.charPress(GLFW_KEY_S, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_D)) {
            viewPole.charPress(GLFW_KEY_D, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_A)) {
            viewPole.charPress(GLFW_KEY_A, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_E)) {
            viewPole.charPress(GLFW_KEY_E, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_Q)) {
            viewPole.charPress(GLFW_KEY_Q, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }


        if (isKeyPressed(GLFW_KEY_I)) {
            lightViewPole.charPress(GLFW_KEY_I, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_K)) {
            lightViewPole.charPress(GLFW_KEY_K, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_L)) {
            lightViewPole.charPress(GLFW_KEY_L, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_J)) {
            lightViewPole.charPress(GLFW_KEY_J, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }

        if (isKeyPressed(GLFW_KEY_O)) {
            lightViewPole.charPress(GLFW_KEY_O, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        } else if (isKeyPressed(GLFW_KEY_U)) {
            lightViewPole.charPress(GLFW_KEY_U, isKeyPressed(GLFW_KEY_LEFT_SHIFT) ||
                    isKeyPressed(GLFW_KEY_RIGHT_SHIFT), lastFrameDuration * scale);
        }
    }

    ////////////////////////////////
    private int coloredModelToCameraMatrixUnif;
    private int coloredProg;

    private int unlitProg;
    private int unlitModelToCameraMatrixUnif;
    private int unlitObjectColorUnif;


    private void loadAndSetupScene() {
        scene = new Scene("proj2d_scene.xml");

        nodes = new ArrayList<>();
        nodes.add(scene.findNode("cube"));
        nodes.add(scene.findNode("rightBar"));
        nodes.add(scene.findNode("leaningBar"));
        nodes.add(scene.findNode("spinBar"));
        nodes.add(scene.findNode("diorama"));
        nodes.add(scene.findNode("floor"));

        lightNumBinder = new UniformIntBinder();
        SceneBinders.associateUniformWithNodes(nodes, lightNumBinder, "numberOfLights");
        SceneBinders.setStateBinderWithNodes(nodes, lightNumBinder);

        lightProjMatBinder = new UniformMat4Binder();
        SceneBinders.associateUniformWithNodes(nodes, lightProjMatBinder, "cameraToLightProjMatrix");
        SceneBinders.setStateBinderWithNodes(nodes, lightProjMatBinder);

        camLightPosBinder = new UniformVec3Binder();
        SceneBinders.associateUniformWithNodes(nodes, camLightPosBinder, "cameraSpaceProjLightPos");
        SceneBinders.setStateBinderWithNodes(nodes, camLightPosBinder);

        int unlit = scene.findProgram("p_unlit");
        sphereMesh = scene.findMesh("m_sphere");

        int colored = scene.findProgram("p_colored");
        axesMesh = scene.findMesh("m_axes");

        // No more things that can throw.
        spinBarOrient = nodes.get(3).nodeGetOrient();
        unlitProg = unlit;
        unlitModelToCameraMatrixUnif = glGetUniformLocation(unlit, "modelToCameraMatrix");
        unlitObjectColorUnif = glGetUniformLocation(unlit, "objectColor");

        coloredProg = colored;
        coloredModelToCameraMatrixUnif = glGetUniformLocation(colored, "modelToCameraMatrix");
    }

    ////////////////////////////////
    private static int displayWidth = 500;
    private static int displayHeight = 500;

    private Scene scene;
    private ArrayList<SceneNode> nodes;
    private Mesh sphereMesh, axesMesh;

    private UniformMat4Binder lightProjMatBinder;
    private UniformVec3Binder camLightPosBinder;

    private float[] lightFOVs = {
            10.0f, 20.0f, 45.0f, 75.0f,
            90.0f, 120.0f, 150.0f, 170.0f
    };
    private int currFOVIndex = 3;

    private Timer timer = new Timer(Timer.Type.LOOP, 10.0f);

    private Quaternionf spinBarOrient;

    private boolean showOtherLights = true;
    private boolean drawCameraPos;

    ////////////////////////////////
    // View setup.
    private ViewData initialView = new ViewData(
            new Vector3f(0.0f, 0.0f, 10.0f),
            new Quaternionf(0.16043f, -0.376867f, -0.0664516f, 0.909845f),
            25.0f,
            0.0f
    );

    private ViewScale initialViewScale = new ViewScale(
            5.0f, 70.0f,
            2.0f, 0.5f,
            2.0f, 0.5f,
            90.0f / 250.0f
    );


    private ViewData initLightView = new ViewData(
            new Vector3f(0.0f, 0.0f, 20.0f),
            new Quaternionf(0.0f, 0.0f, 0.0f, 1.0f),
            5.0f,
            0.0f
    );

    private ViewScale initLightViewScale = new ViewScale(
            0.05f, 10.0f,
            0.1f, 0.05f,
            4.0f, 1.0f,
            90.0f / 250.0f
    );


    private ViewPole viewPole = new ViewPole(initialView, initialViewScale, MouseButtons.MB_LEFT_BTN);
    private ViewPole lightViewPole = new ViewPole(initLightView, initLightViewScale, MouseButtons.MB_RIGHT_BTN, true);

    ////////////////////////////////
    private final TexDef[] texDefs = {
            new TexDef("Flashlight.dds", "Flashlight"),
            new TexDef("PointsOfLight.dds", "Multiple Point Lights"),
            new TexDef("Bands.dds", "Light Bands")
    };
    private final int NUM_LIGHT_TEXTURES = texDefs.length;
    private int[] lightTextures = new int[texDefs.length];
    private int currTextureIndex = 0;

    private class TexDef {
        String filename;
        String name;

        TexDef(String filename, String name) {
            this.filename = filename;
            this.name = name;
        }
    }


    private void loadTextures() {
        try {
            for (int textureIndex = 0; textureIndex < NUM_LIGHT_TEXTURES; textureIndex++) {
                String filePath = Framework.findFileOrThrow(texDefs[textureIndex].filename);
                ImageSet imageSet = DdsLoader.loadFromFile(filePath);
                lightTextures[textureIndex] = TextureGenerator.createTexture(imageSet, 0);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    ////////////////////////////////
    private final int NUM_SAMPLERS = 2;

    private int[] samplers = new int[NUM_SAMPLERS];
    private int currSampler = 0;


    private void createSamplers() {
        for (int samplerIndex = 0; samplerIndex < NUM_SAMPLERS; samplerIndex++) {
            samplers[samplerIndex] = glGenSamplers();
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glSamplerParameteri(samplers[samplerIndex], GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        }

        glSamplerParameteri(samplers[0], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(samplers[0], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        glSamplerParameteri(samplers[1], GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
        glSamplerParameteri(samplers[1], GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);


        float[] color = {0.0f, 0.0f, 0.0f, 1.0f};
        FloatBuffer colorBuffer = BufferUtils.createFloatBuffer(4);

        for (float f : color) {
            colorBuffer.put(f);
        }

        colorBuffer.flip();

        glSamplerParameterfv(samplers[1], GL_TEXTURE_BORDER_COLOR, colorBuffer);
    }

    ////////////////////////////////
    private final int projectionBlockIndex = 0;

    private int projectionUniformBuffer;

    ////////////////////////////////
    private final int lightBlockIndex = 1;
    private final int lightProjTexUnit = 3;

    private int lightUniformBuffer;
    private UniformIntBinder lightNumBinder;


    private void buildLights(Matrix4f camMatrix) {
        LightBlock lightData = new LightBlock();
        lightData.ambientIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        lightData.lightAttenuation = 1.0f / (30.0f * 30.0f);
        lightData.maxIntensity = 2.0f;

        lightData.lights[0] = new PerLight();
        lightData.lights[0].lightIntensity = new Vector4f(0.2f, 0.2f, 0.2f, 1.0f);
        lightData.lights[0].cameraSpaceLightPos = camMatrix.transform(new Vector4f(-0.2f, 0.5f, 0.5f, 0.0f).normalize());

        lightData.lights[1] = new PerLight();
        lightData.lights[1].lightIntensity = new Vector4f(3.5f, 6.5f, 3.0f, 1.0f).mul(0.5f);
        lightData.lights[1].cameraSpaceLightPos = camMatrix.transform(new Vector4f(5.0f, 6.0f, 0.5f, 1.0f));

        if (showOtherLights) {
            lightNumBinder.setValue(2);
        } else {
            lightNumBinder.setValue(0);
        }

        glBindBuffer(GL_UNIFORM_BUFFER, lightUniformBuffer);
        glBufferData(GL_UNIFORM_BUFFER, lightData.getAndFlip(lightBlockBuffer), GL_STREAM_DRAW);
    }
}
import integeruser.jgltut.framework.Framework;
import integeruser.jgltut.tut01.Tut1;
import integeruser.jgltut.tut02.FragPosition;
import integeruser.jgltut.tut02.VertexColor;
import integeruser.jgltut.tut03.CpuPositionOffset;
import integeruser.jgltut.tut03.FragChangeColor;
import integeruser.jgltut.tut03.VertCalcOffset;
import integeruser.jgltut.tut03.VertPositionOffset;
import integeruser.jgltut.tut04.AspectRatio;
import integeruser.jgltut.tut04.MatrixPerspective;
import integeruser.jgltut.tut04.OrthoCube;
import integeruser.jgltut.tut04.ShaderPerspective;
import integeruser.jgltut.tut05.*;
import integeruser.jgltut.tut06.Hierarchy;
import integeruser.jgltut.tut06.Rotation;
import integeruser.jgltut.tut06.Scale;
import integeruser.jgltut.tut06.Translation;
import integeruser.jgltut.tut07.WorldScene;
import integeruser.jgltut.tut07.WorldWithUBO;
import integeruser.jgltut.tut08.CameraRelative;
import integeruser.jgltut.tut08.GimbalLock;
import integeruser.jgltut.tut08.Interpolation;
import integeruser.jgltut.tut08.QuaternionYPR;
import integeruser.jgltut.tut09.AmbientLighting;
import integeruser.jgltut.tut09.BasicLighting;
import integeruser.jgltut.tut09.ScaleAndLighting;
import integeruser.jgltut.tut10.FragmentAttenuation;
import integeruser.jgltut.tut10.FragmentPointLighting;
import integeruser.jgltut.tut10.VertexPointLighting;
import integeruser.jgltut.tut11.BlinnVsPhongLighting;
import integeruser.jgltut.tut11.GaussianSpecularLighting;
import integeruser.jgltut.tut11.PhongLighting;
import integeruser.jgltut.tut12.GammaCorrection;
import integeruser.jgltut.tut12.HDRLighting;
import integeruser.jgltut.tut12.SceneLighting;
import integeruser.jgltut.tut13.BasicImpostor;
import integeruser.jgltut.tut13.GeomImpostor;
import integeruser.jgltut.tut14.BasicTexture;
import integeruser.jgltut.tut14.MaterialTexture;
import integeruser.jgltut.tut14.PerspectiveInterpolation;
import integeruser.jgltut.tut15.ManyImages;
import integeruser.jgltut.tut16.GammaCheckers;
import integeruser.jgltut.tut16.GammaLandscape;
import integeruser.jgltut.tut16.GammaRamp;
import integeruser.jgltut.tut17.CubePointLight;
import integeruser.jgltut.tut17.DoubleProjection;
import integeruser.jgltut.tut17.ProjectedLight;
import javax.swing.*;
import javax.swing.event.TreeSelectionEvent;
import javax.swing.event.TreeSelectionListener;
import javax.swing.tree.DefaultMutableTreeNode;
import java.awt.*;
package integeruser.jgltut;




/**
 * Visit https://github.com/integeruser/jgltut for info and updates.
 */
public class TutorialChooser extends JPanel implements TreeSelectionListener {
    public static void main(String[] args) {
        JFrame frame = new JFrame("Click on a tutorial to run it");
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        JPanel tutorialsViewer = new TutorialChooser();
        frame.setContentPane(tutorialsViewer);

        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }


    private TutorialChooser() {
        DefaultMutableTreeNode top = new DefaultMutableTreeNode("jgltut");
        createNodes(top);

        tree = new JTree(top);
        tree.addTreeSelectionListener(this);

        JScrollPane treeView = new JScrollPane(tree);
        treeView.setPreferredSize(new Dimension(350, 400));
        add(treeView);
    }


    @Override
    public void valueChanged(TreeSelectionEvent e) {
        DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();

        if (node != null && node.isLeaf()) {
            final String selectedTutorial = (String) node.getUserObject();

            new Thread(new Runnable() {
                @Override
                public void run() {
                    switch (selectedTutorial) {
                        case "Tut1":
                            new Tut1().start(500, 500);
                            break;

                        case "FragPosition":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut02/data/";
                            new FragPosition().start(500, 500);
                            break;
                        case "VertexColor":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut02/data/";
                            new VertexColor().start(500, 500);
                            break;


                        case "CpuPositionOffset":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
                            new CpuPositionOffset().start(500, 500);
                            break;
                        case "VertPositionOffset":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
                            new VertPositionOffset().start(500, 500);
                            break;
                        case "VertCalcOffset":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
                            new VertCalcOffset().start(500, 500);
                            break;
                        case "FragChangeColor":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut03/data/";
                            new FragChangeColor().start(500, 500);
                            break;

                        case "OrthoCube":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
                            new OrthoCube().start(500, 500);
                            break;
                        case "ShaderPerspective":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
                            new ShaderPerspective().start(500, 500);
                            break;
                        case "MatrixPerspective":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
                            new MatrixPerspective().start(500, 500);
                            break;
                        case "AspectRatio":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut04/data/";
                            new AspectRatio().start(500, 500);
                            break;

                        case "OverlapNoDepth":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut05/data/";
                            new OverlapNoDepth().start(500, 500);
                            break;
                        case "BaseVertexOverlap":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut05/data/";
                            new BaseVertexOverlap().start(500, 500);
                            break;
                        case "DepthBuffer":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut05/data/";
                            new DepthBuffer().start(500, 500);
                            break;
                        case "VertexClipping":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut05/data/";
                            new VertexClipping().start(500, 500);
                            break;
                        case "DepthClamping":
                            Framework.CURRENT_TUTORIAL_DATAPATH = "/integeruser/jgltut/tut05/data/";
                            new DepthClamping().start(500, 500);
                            break;

                        case "Translation":