org.lwjgl.demo.opengl.raytracing.PhotonMappingBindlessDemo.java Source code

Java tutorial

Introduction

Here is the source code for org.lwjgl.demo.opengl.raytracing.PhotonMappingBindlessDemo.java

Source

/*
 * Copyright LWJGL. All rights reserved.
 * License terms: http://lwjgl.org/license.php
 */
package org.lwjgl.demo.opengl.raytracing;

import org.lwjgl.BufferUtils;
import org.lwjgl.demo.opengl.util.DemoUtils;
import org.lwjgl.glfw.*;
import org.lwjgl.opengl.ARBBindlessTexture;
import org.lwjgl.opengl.ARBClearTexture;
import org.lwjgl.opengl.ARBComputeVariableGroupSize;
import org.lwjgl.opengl.GL;
import org.lwjgl.opengl.GLCapabilities;
import org.joml.Matrix4f;
import org.joml.Vector3f;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.nio.LongBuffer;

import static java.lang.Math.*;
import static org.lwjgl.glfw.GLFW.*;
import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.*;
import static org.lwjgl.opengl.GL13.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL21.*;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL31.*;
import static org.lwjgl.opengl.GL33.*;
import static org.lwjgl.opengl.GL42.*;
import static org.lwjgl.opengl.GL43.*;
import static org.lwjgl.system.MathUtil.*;
import static org.lwjgl.system.MemoryUtil.*;

/**
 * Photon mapping using bindless cube map textures.
 * <p>
 * The same as {@link PhotonMappingDemo} BUT it uses ARB_bindless_texture to
 * have an array of cube map textures without the restriction of equal
 * dimensions for each cube map. We want each cube map to have the dimension in
 * proportion to the actual size of the cube it is mapped to.
 * 
 * @author Kai Burjack
 */
public class PhotonMappingBindlessDemo {

    class TextureInfo {
        int textureWidth;
        int textureHeight;
        int openGlHandle;
        long bindlessImageHandle;
        long bindlessTextureAndSamplerHandle;
    }

    /**
     * The boxes for both rasterization and ray tracing.
     */
    private static Vector3f[] boxes = Scene.boxes;

    /**
     * The number of texels to use for one unit of length in the scene.
     */
    private static int INITIAL_TEXELS_PER_UNIT = 8;
    private static int MAX_TEXELS_PER_UNIT = 128;

    /**
     * The number of photons to trace when doing a single photonmap computation.
     * This is actually the square root of the number of photons, since we are
     * using both dimension X and Y of the compute shader, with equal number of
     * work items.
     */
    private static int INITIAL_PHOTONS_PER_FRAME = 16;
    private static int MAX_PHOTONS_PER_FRAME = 2048;

    private GLCapabilities caps;

    private long window;
    private int width = 1024;
    private int height = 768;
    private boolean resetFramebuffer = true;
    private boolean clearPhotonMapTexture = false;
    private boolean recreatePhotonMapTextures = false;
    private int texelsPerUnit = INITIAL_TEXELS_PER_UNIT;
    private int photonsPerFrame = INITIAL_PHOTONS_PER_FRAME;

    private TextureInfo[] photonMapTextures = new TextureInfo[boxes.length / 2];
    private int photonTraceProgram;
    private int rasterProgram;
    private int vaoScene;
    private int ssbo;
    private int imageHandlesUbo;
    private int samplersUbo;
    private int sampler;

    private int timeUniform;
    private int lightCenterPositionUniform;
    private int lightRadiusUniform;
    private int boxesSsboBinding;
    private int imagesUboBinding;
    private int samplersUboBinding;

    private int viewMatrixUniform;
    private int projectionMatrixUniform;

    private int workGroupSizeX = 8;
    private int workGroupSizeY = 8;
    private boolean variableGroupSize;

    private float mouseDownX;
    private float mouseX;
    private boolean mouseDown;

    private float currRotationAboutY = 0.0f;
    private float rotationAboutY = 0.8f;

    private long firstTime;
    private float lightRadius = 0.2f;

    private Matrix4f projMatrix = new Matrix4f();
    private Matrix4f viewMatrix = new Matrix4f();
    private Vector3f cameraPosition = new Vector3f();
    private Vector3f cameraLookAt = new Vector3f(0.0f, 0.5f, 0.0f);
    private Vector3f cameraUp = new Vector3f(0.0f, 1.0f, 0.0f);
    private FloatBuffer matrixBuffer = BufferUtils.createFloatBuffer(16);
    private Vector3f lightCenterPosition = new Vector3f(2.5f, 1.4f, 3);
    private ByteBuffer clearTexBuffer = BufferUtils.createByteBuffer(4);

    GLFWErrorCallback errCallback;
    GLFWKeyCallback keyCallback;
    GLFWFramebufferSizeCallback fbCallback;
    GLFWCursorPosCallback cpCallback;
    GLFWMouseButtonCallback mbCallback;

    private void init() throws IOException {
        glfwSetErrorCallback(errCallback = new GLFWErrorCallback() {
            private GLFWErrorCallback delegate = GLFWErrorCallback.createPrint(System.err);

            @Override
            public void invoke(int error, long description) {
                if (error == GLFW_VERSION_UNAVAILABLE)
                    System.err.println("This demo requires OpenGL 4.3 or higher.");
                delegate.invoke(error, description);
            }

            @Override
            public void free() {
                delegate.free();
            }
        });

        if (!glfwInit())
            throw new IllegalStateException("Unable to initialize GLFW");

        glfwDefaultWindowHints();
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GLFW_TRUE);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);
        glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);

        window = glfwCreateWindow(width, height, "Photon Mapping Demo - bindless", NULL, NULL);
        if (window == NULL) {
            throw new AssertionError("Failed to create the GLFW window");
        }

        System.out.println("Press 'r' to clear the photon map.");
        System.out.println("Press arrow 'up' to increase photon map resolution.");
        System.out.println("Press arrow 'down' to decrease the photon map resolution.");
        System.out.println("Press arrow 'right' to increase number of photons per frame.");
        System.out.println("Press arrow 'left' to decrease number of photons per frame.");
        glfwSetKeyCallback(window, keyCallback = new GLFWKeyCallback() {
            @Override
            public void invoke(long window, int key, int scancode, int action, int mods) {
                if (action != GLFW_RELEASE)
                    return;

                if (key == GLFW_KEY_ESCAPE) {
                    glfwSetWindowShouldClose(window, true);
                } else if (key == GLFW_KEY_R) {
                    PhotonMappingBindlessDemo.this.clearPhotonMapTexture = true;
                } else if (key == GLFW_KEY_UP) {
                    PhotonMappingBindlessDemo.this.texelsPerUnit *= 2;
                    PhotonMappingBindlessDemo.this.texelsPerUnit = Math
                            .min(PhotonMappingBindlessDemo.this.texelsPerUnit, MAX_TEXELS_PER_UNIT);
                    PhotonMappingBindlessDemo.this.recreatePhotonMapTextures = true;
                    System.out.println("Photon map resolution (texels per unit): "
                            + PhotonMappingBindlessDemo.this.texelsPerUnit);
                } else if (key == GLFW_KEY_DOWN) {
                    PhotonMappingBindlessDemo.this.texelsPerUnit /= 2;
                    PhotonMappingBindlessDemo.this.texelsPerUnit = Math
                            .max(PhotonMappingBindlessDemo.this.texelsPerUnit, 4);
                    PhotonMappingBindlessDemo.this.recreatePhotonMapTextures = true;
                    System.out.println("Photon map resolution (texels per unit): "
                            + PhotonMappingBindlessDemo.this.texelsPerUnit);
                } else if (key == GLFW_KEY_RIGHT) {
                    PhotonMappingBindlessDemo.this.photonsPerFrame *= 2;
                    PhotonMappingBindlessDemo.this.photonsPerFrame = Math
                            .min(PhotonMappingBindlessDemo.this.photonsPerFrame, MAX_PHOTONS_PER_FRAME);
                    System.out.println("Photons per frame: " + PhotonMappingBindlessDemo.this.photonsPerFrame);
                } else if (key == GLFW_KEY_LEFT) {
                    PhotonMappingBindlessDemo.this.photonsPerFrame /= 2;
                    PhotonMappingBindlessDemo.this.photonsPerFrame = Math.max(
                            PhotonMappingBindlessDemo.this.photonsPerFrame,
                            Math.max(workGroupSizeX, workGroupSizeY));
                    System.out.println("Photons per frame: " + PhotonMappingBindlessDemo.this.photonsPerFrame);
                }
            }
        });

        glfwSetFramebufferSizeCallback(window, fbCallback = new GLFWFramebufferSizeCallback() {
            @Override
            public void invoke(long window, int width, int height) {
                if (width > 0 && height > 0 && (PhotonMappingBindlessDemo.this.width != width
                        || PhotonMappingBindlessDemo.this.height != height)) {
                    PhotonMappingBindlessDemo.this.width = width;
                    PhotonMappingBindlessDemo.this.height = height;
                    PhotonMappingBindlessDemo.this.resetFramebuffer = true;
                }
            }
        });

        glfwSetCursorPosCallback(window, cpCallback = new GLFWCursorPosCallback() {
            @Override
            public void invoke(long window, double x, double y) {
                PhotonMappingBindlessDemo.this.mouseX = (float) x;
            }
        });

        glfwSetMouseButtonCallback(window, mbCallback = new GLFWMouseButtonCallback() {
            @Override
            public void invoke(long window, int button, int action, int mods) {
                if (action == GLFW_PRESS) {
                    PhotonMappingBindlessDemo.this.mouseDownX = PhotonMappingBindlessDemo.this.mouseX;
                    PhotonMappingBindlessDemo.this.mouseDown = true;
                } else if (action == GLFW_RELEASE) {
                    PhotonMappingBindlessDemo.this.mouseDown = false;
                    PhotonMappingBindlessDemo.this.rotationAboutY = PhotonMappingBindlessDemo.this.currRotationAboutY;
                }
            }
        });

        GLFWVidMode vidmode = glfwGetVideoMode(glfwGetPrimaryMonitor());
        glfwSetWindowPos(window, (vidmode.width() - width) / 2, (vidmode.height() - height) / 2);
        glfwMakeContextCurrent(window);
        glfwSwapInterval(0);
        glfwShowWindow(window);

        IntBuffer framebufferSize = BufferUtils.createIntBuffer(2);
        nglfwGetFramebufferSize(window, memAddress(framebufferSize), memAddress(framebufferSize) + 4);
        width = framebufferSize.get(0);
        height = framebufferSize.get(1);

        caps = GL.createCapabilities();

        if (!caps.GL_ARB_bindless_texture)
            throw new RuntimeException("This demo requires the ARB_bindless_texture extension.");

        /* Create all needed GL resources */
        createSampler();
        createImageHandlesUbo();
        createSamplerHandlesUbo();
        createPhotonMapTextures();
        createPhotonTraceProgram();
        initPhotonTraceProgram();
        createRasterProgram();
        initRasterProgram();
        createSceneSSBO();
        createSceneVao();

        /* Set some state */
        glEnable(GL_DEPTH_TEST);
        glEnable(GL_CULL_FACE);

        firstTime = System.nanoTime();
    }

    /**
     * Create a Shader Storage Buffer Object which will hold our boxes to be
     * read by our Compute Shader.
     */
    private void createSceneSSBO() {
        this.ssbo = glGenBuffers();
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, ssbo);
        ByteBuffer ssboData = BufferUtils.createByteBuffer(4 * (4 + 4) * boxes.length / 2);
        FloatBuffer fv = ssboData.asFloatBuffer();
        for (int i = 0; i < boxes.length; i += 2) {
            Vector3f min = boxes[i];
            Vector3f max = boxes[i + 1];
            /*
             * NOTE: We need to write vec4 here, because SSBOs have specific
             * alignment requirements for struct members (vec3 is always treated
             * as vec4 in memory!)
             * 
             * See:
             * "https://www.safaribooksonline.com/library/view/opengl-programming-guide/9780132748445/app09lev1sec3.html"
             */
            fv.put(min.x).put(min.y).put(min.z).put(0.0f);
            fv.put(max.x).put(max.y).put(max.z).put(0.0f);
        }
        glBufferData(GL_SHADER_STORAGE_BUFFER, ssboData, GL_STATIC_DRAW);
        glBindBuffer(GL_SHADER_STORAGE_BUFFER, 0);
    }

    /**
     * Creates a VAO for the scene.
     */
    private void createSceneVao() {
        int vao = glGenVertexArrays();

        /* Create vertex data */
        int vbo = glGenBuffers();
        glBindVertexArray(vao);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        ByteBuffer bb = BufferUtils.createByteBuffer(4 * (3 + 3) * 6 * 6);
        FloatBuffer fv = bb.asFloatBuffer();
        DemoUtils.triangulateUnitBox(fv);
        glBufferData(GL_ARRAY_BUFFER, bb, GL_STATIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, false, 4 * (3 + 3), 0L);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 3, GL_FLOAT, false, 4 * (3 + 3), 4 * 3);
        glBindBuffer(GL_ARRAY_BUFFER, 0);

        /* Create per instance data (position and size of box) */
        int ivbo = glGenBuffers();
        glBindBuffer(GL_ARRAY_BUFFER, ivbo);
        bb = BufferUtils.createByteBuffer(4 * (3 + 3) * boxes.length);
        fv = bb.asFloatBuffer();
        for (int i = 0; i < boxes.length; i += 2) {
            Vector3f min = boxes[i];
            Vector3f max = boxes[i + 1];
            fv.put((max.x + min.x) / 2.0f).put((max.y + min.y) / 2.0f).put((max.z + min.z) / 2.0f);
            fv.put((max.x - min.x) / 2.0f).put((max.y - min.y) / 2.0f).put((max.z - min.z) / 2.0f);
        }
        glBufferData(GL_ARRAY_BUFFER, bb, GL_STATIC_DRAW);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 3, GL_FLOAT, false, 4 * (3 + 3), 0L);
        glVertexAttribDivisor(2, 1);
        glEnableVertexAttribArray(3);
        glVertexAttribPointer(3, 3, GL_FLOAT, false, 4 * (3 + 3), 4 * 3);
        glVertexAttribDivisor(3, 1);
        glBindBuffer(GL_ARRAY_BUFFER, 0);
        glBindVertexArray(0);
        this.vaoScene = vao;
    }

    private void createPhotonTraceProgram() throws IOException {
        int program = glCreateProgram();
        int cshader = DemoUtils.createShader("org/lwjgl/demo/opengl/raytracing/photonmapBindless.glsl",
                GL_COMPUTE_SHADER);
        int random = DemoUtils.createShader("org/lwjgl/demo/opengl/raytracing/random.glsl", GL_COMPUTE_SHADER);
        int randomCommon = DemoUtils.createShader("org/lwjgl/demo/opengl/raytracing/randomCommon.glsl",
                GL_COMPUTE_SHADER, "330");
        glAttachShader(program, cshader);
        glAttachShader(program, random);
        glAttachShader(program, randomCommon);
        glLinkProgram(program);
        int linked = glGetProgrami(program, GL_LINK_STATUS);
        String programLog = glGetProgramInfoLog(program);
        if (programLog.trim().length() > 0) {
            System.err.println(programLog);
        }
        if (linked == 0) {
            throw new AssertionError("Could not link program");
        }
        this.photonTraceProgram = program;
    }

    private void initPhotonTraceProgram() {
        glUseProgram(photonTraceProgram);

        IntBuffer workGroupSize = BufferUtils.createIntBuffer(3);
        glGetProgramiv(photonTraceProgram, GL_COMPUTE_WORK_GROUP_SIZE, workGroupSize);
        int staticWorkGroupSizeX = workGroupSize.get(0);
        int staticWorkGroupSizeY = workGroupSize.get(1);
        if (caps.GL_ARB_compute_variable_group_size) {
            /* We can dynamically set the work group size! */
            variableGroupSize = true;
        }
        if (staticWorkGroupSizeX > 0 && staticWorkGroupSizeY > 0) {
            /*
             * If the shader specified a work group size (if variable group size
             * is not available), use that.
             */
            workGroupSizeX = staticWorkGroupSizeX;
            workGroupSizeY = staticWorkGroupSizeY;
        }

        timeUniform = glGetUniformLocation(photonTraceProgram, "time");
        lightCenterPositionUniform = glGetUniformLocation(photonTraceProgram, "lightCenterPosition");
        glUniform3f(lightCenterPositionUniform, lightCenterPosition.x, lightCenterPosition.y,
                lightCenterPosition.z);
        lightRadiusUniform = glGetUniformLocation(photonTraceProgram, "lightRadius");
        glUniform1f(lightRadiusUniform, lightRadius);

        /* Query the binding point of the SSBO */
        /*
         * First, obtain the "resource index" used for further queries on that
         * resource.
         */
        int boxesResourceIndex = glGetProgramResourceIndex(photonTraceProgram, GL_SHADER_STORAGE_BLOCK, "Boxes");
        IntBuffer props = BufferUtils.createIntBuffer(1);
        IntBuffer params = BufferUtils.createIntBuffer(1);
        props.put(0, GL_BUFFER_BINDING);
        glGetProgramResourceiv(photonTraceProgram, GL_SHADER_STORAGE_BLOCK, boxesResourceIndex, props, null,
                params);
        boxesSsboBinding = params.get(0);
        int imagesResourceIndex = glGetProgramResourceIndex(photonTraceProgram, GL_UNIFORM_BLOCK, "Images");
        glGetProgramResourceiv(photonTraceProgram, GL_UNIFORM_BLOCK, imagesResourceIndex, props, null, params);
        imagesUboBinding = params.get(0);

        glUseProgram(0);
    }

    /**
     * Create the cubemap texture array for our photon map.
     */
    private void createPhotonMapTextures() {
        /* Create them */
        IntBuffer textures = BufferUtils.createIntBuffer(photonMapTextures.length);
        glGenTextures(textures);
        for (int i = 0; i < photonMapTextures.length; i++) {
            TextureInfo info = new TextureInfo();
            info.openGlHandle = textures.get(i);
            Vector3f min = boxes[2 * i + 0];
            Vector3f max = boxes[2 * i + 1];
            float maxExtent = Math.max(Math.max(max.x - min.x, max.y - min.y), max.z - min.z);
            int texSize = (int) (maxExtent * texelsPerUnit);
            info.textureWidth = texSize;
            info.textureHeight = texSize;
            glBindTexture(GL_TEXTURE_CUBE_MAP, info.openGlHandle);
            glTexStorage2D(GL_TEXTURE_CUBE_MAP, 1, GL_RG16F, texSize, texSize);
            glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
            info.bindlessImageHandle = ARBBindlessTexture.glGetImageHandleARB(info.openGlHandle, 0, true, 0,
                    GL_RG16F);
            info.bindlessTextureAndSamplerHandle = ARBBindlessTexture
                    .glGetTextureSamplerHandleARB(info.openGlHandle, sampler);
            ARBBindlessTexture.glMakeImageHandleResidentARB(info.bindlessImageHandle, GL_READ_WRITE);
            ARBBindlessTexture.glMakeTextureHandleResidentARB(info.bindlessTextureAndSamplerHandle);
            photonMapTextures[i] = info;
        }
        /* Clear them */
        clearPhotonMapTextures();
        /* Update SSBO with bindless image handles */
        updateImageHandlesUbo();
        /* Update UBO with bindless sampler handles */
        updateSamplerHandlesUbo();
    }

    private void createImageHandlesUbo() {
        this.imageHandlesUbo = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, imageHandlesUbo);
        glBufferData(GL_UNIFORM_BUFFER, photonMapTextures.length * 8, GL_STATIC_DRAW);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);
    }

    private void updateImageHandlesUbo() {
        ByteBuffer ssboData = BufferUtils.createByteBuffer(photonMapTextures.length * 8);
        LongBuffer lv = ssboData.asLongBuffer();
        for (int i = 0; i < photonMapTextures.length; i++) {
            TextureInfo info = photonMapTextures[i];
            long bindlessImageHandle = info.bindlessImageHandle;
            lv.put(bindlessImageHandle);
        }
        glBindBuffer(GL_UNIFORM_BUFFER, this.imageHandlesUbo);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, ssboData);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);
    }

    private void createSamplerHandlesUbo() {
        this.samplersUbo = glGenBuffers();
        glBindBuffer(GL_UNIFORM_BUFFER, samplersUbo);
        glBufferData(GL_UNIFORM_BUFFER, photonMapTextures.length * 8, GL_STATIC_DRAW);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);
    }

    private void updateSamplerHandlesUbo() {
        ByteBuffer ssboData = BufferUtils.createByteBuffer(photonMapTextures.length * 8);
        LongBuffer lv = ssboData.asLongBuffer();
        for (int i = 0; i < photonMapTextures.length; i++) {
            lv.put(photonMapTextures[i].bindlessTextureAndSamplerHandle);
        }
        glBindBuffer(GL_UNIFORM_BUFFER, samplersUbo);
        glBufferSubData(GL_UNIFORM_BUFFER, 0, ssboData);
        glBindBuffer(GL_UNIFORM_BUFFER, 0);
    }

    /**
     * Clear the photon map textures.
     */
    private void clearPhotonMapTextures() {
        /*
         * Clear the first level of the texture with black without allocating
         * host memory.
         */
        if (caps.GL_ARB_clear_texture) {
            /*
             * If ARB_clear_texture is available, we can directly clear the
             * image of the texture. A version where you can clear the images of
             * multiple successive textures would be handy here!
             */
            for (int i = 0; i < photonMapTextures.length; i++) {
                TextureInfo info = photonMapTextures[i];
                ARBClearTexture.glClearTexImage(info.openGlHandle, 0, GL_RG, GL_HALF_FLOAT, clearTexBuffer);
            }
        } else {
            /*
             * If not, we create a temporary buffer object and use pixel unpack
             * to move GPU memory from that buffer to the texture image.
             */
            for (int i = 0; i < photonMapTextures.length; i++) {
                TextureInfo info = photonMapTextures[i];
                int texBuffer = glGenBuffers();
                glBindBuffer(GL_PIXEL_UNPACK_BUFFER, texBuffer);
                int size = 2 * 2 * info.textureWidth * info.textureHeight;
                glBufferData(GL_PIXEL_UNPACK_BUFFER, size, GL_STATIC_DRAW);
                glClearBufferSubData(GL_PIXEL_UNPACK_BUFFER, GL_RG16F, 0, size, GL_RG, GL_HALF_FLOAT,
                        (ByteBuffer) null);
                glBindTexture(GL_TEXTURE_CUBE_MAP, info.openGlHandle);
                for (int f = 0; f < 6; f++) {
                    glTexSubImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + f, 0, 0, 0, info.textureWidth,
                            info.textureHeight, GL_RG, GL_HALF_FLOAT, 0L);
                }
                glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
                glBindBuffer(GL_PIXEL_UNPACK_BUFFER, 0);
                glDeleteBuffers(texBuffer);
            }
        }
    }

    /**
     * Recreate the photon map texture.
     */
    private void recreatePhotonMapTexture() {
        for (int i = 0; i < photonMapTextures.length; i++) {
            TextureInfo info = photonMapTextures[i];
            if (info != null) {
                ARBBindlessTexture.glMakeImageHandleNonResidentARB(info.bindlessImageHandle);
                ARBBindlessTexture.glMakeTextureHandleNonResidentARB(info.bindlessTextureAndSamplerHandle);
                glDeleteTextures(info.openGlHandle);
            }
        }
        createPhotonMapTextures();
    }

    /**
     * Create the sampler to sample the cube map textures within the shader.
     */
    private void createSampler() {
        this.sampler = glGenSamplers();
        glSamplerParameteri(this.sampler, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(this.sampler, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(this.sampler, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
        glSamplerParameteri(this.sampler, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
        glSamplerParameteri(this.sampler, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    }

    /**
     * Create the raster shader.
     *
     * @throws IOException
     */
    private void createRasterProgram() throws IOException {
        int program = glCreateProgram();
        int vshader = DemoUtils.createShader("org/lwjgl/demo/opengl/raytracing/rasterPhotonMap.vs",
                GL_VERTEX_SHADER);
        int fshader = DemoUtils.createShader("org/lwjgl/demo/opengl/raytracing/rasterPhotonMapBindless.fs",
                GL_FRAGMENT_SHADER);
        glAttachShader(program, vshader);
        glAttachShader(program, fshader);
        glBindAttribLocation(program, 0, "vertexPosition");
        glBindAttribLocation(program, 1, "vertexNormal");
        glBindAttribLocation(program, 2, "boxCenter");
        glBindAttribLocation(program, 3, "boxHalfSize");
        glBindFragDataLocation(program, 0, "color");
        glLinkProgram(program);
        int linked = glGetProgrami(program, GL_LINK_STATUS);
        String programLog = glGetProgramInfoLog(program);
        if (programLog.trim().length() > 0) {
            System.err.println(programLog);
        }
        if (linked == 0) {
            throw new AssertionError("Could not link program");
        }
        this.rasterProgram = program;
    }

    private void initRasterProgram() {
        glUseProgram(rasterProgram);
        viewMatrixUniform = glGetUniformLocation(rasterProgram, "viewMatrix");
        projectionMatrixUniform = glGetUniformLocation(rasterProgram, "projectionMatrix");
        glUseProgram(0);
        /* Get binding point of "Samplers" uniform block */
        IntBuffer props = BufferUtils.createIntBuffer(1);
        IntBuffer params = BufferUtils.createIntBuffer(1);
        props.put(0, GL_BUFFER_BINDING);
        int samplersResourceIndex = glGetProgramResourceIndex(rasterProgram, GL_UNIFORM_BLOCK, "Samplers");
        glGetProgramResourceiv(rasterProgram, GL_UNIFORM_BLOCK, samplersResourceIndex, props, null, params);
        samplersUboBinding = params.get(0);
    }

    private void update() {
        if (mouseDown) {
            /*
             * If mouse is down, compute the camera rotation based on mouse
             * cursor location.
             */
            currRotationAboutY = rotationAboutY + (mouseX - mouseDownX) * 0.01f;
        } else {
            currRotationAboutY = rotationAboutY;
        }

        /* Rotate camera about Y axis. */
        cameraPosition.set((float) sin(-currRotationAboutY) * 3.0f, 2.0f, (float) cos(-currRotationAboutY) * 3.0f);
        viewMatrix.setLookAt(cameraPosition, cameraLookAt, cameraUp);

        if (resetFramebuffer) {
            projMatrix.setPerspective((float) Math.toRadians(60.0f), (float) width / height, 0.01f, 100f);
            resetFramebuffer = false;
        }
        if (recreatePhotonMapTextures) {
            recreatePhotonMapTexture();
            recreatePhotonMapTextures = false;
            clearPhotonMapTexture = false;
        } else if (clearPhotonMapTexture) {
            clearPhotonMapTextures();
            clearPhotonMapTexture = false;
        }
    }

    /**
     * Trace some rays from the light.
     */
    private void trace() {
        glUseProgram(photonTraceProgram);

        long thisTime = System.nanoTime();
        float elapsedSeconds = (thisTime - firstTime) / 1E9f;
        glUniform1f(timeUniform, elapsedSeconds);

        /* Bind the SSBO containing our boxes */
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, boxesSsboBinding, ssbo);
        /* Bind the UBO containing the bindless handles of our images */
        glBindBufferBase(GL_UNIFORM_BUFFER, imagesUboBinding, imageHandlesUbo);

        /* Compute appropriate invocation dimension. */
        int invocationsPerDimension = photonsPerFrame;
        int worksizeX = mathRoundPoT(invocationsPerDimension);
        int worksizeY = mathRoundPoT(invocationsPerDimension);

        /* Invoke the compute shader. */
        if (variableGroupSize) {
            ARBComputeVariableGroupSize.glDispatchComputeGroupSizeARB(worksizeX / workGroupSizeX,
                    worksizeY / workGroupSizeY, 1, workGroupSizeX, workGroupSizeY, 1);
        } else {
            glDispatchCompute(worksizeX / workGroupSizeX, worksizeY / workGroupSizeY, 1);
        }
        /*
         * Synchronize all writes that the shader did on the photonMap cube map
         * array image before we later let OpenGL source texels from it when
         * rasterizing the scene and sampling the cube maps in the fragment
         * shader.
         */
        glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT);

        /* Reset bindings. */
        glBindBufferBase(GL_SHADER_STORAGE_BUFFER, boxesSsboBinding, 0);
        glBindBufferBase(GL_UNIFORM_BUFFER, imagesUboBinding, 0);
        glUseProgram(0);
    }

    /**
     * Rasterize the boxes by sampling the traced photon maps and present the
     * final image on the screen/viewport.
     */
    private void raster() {
        glUseProgram(rasterProgram);

        /* Update matrices in shader */
        glUniformMatrix4fv(viewMatrixUniform, false, viewMatrix.get(matrixBuffer));
        glUniformMatrix4fv(projectionMatrixUniform, false, projMatrix.get(matrixBuffer));

        glBindVertexArray(vaoScene);
        glBindBufferBase(GL_UNIFORM_BUFFER, samplersUboBinding, samplersUbo);
        glDrawArraysInstanced(GL_TRIANGLES, 0, 6 * 6, boxes.length / 2);
        glBindBufferBase(GL_UNIFORM_BUFFER, samplersUboBinding, 0);
        glBindVertexArray(0);
        glUseProgram(0);
    }

    private void loop() {
        while (!glfwWindowShouldClose(window)) {
            glfwPollEvents();
            glViewport(0, 0, width, height);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

            update();
            trace();
            raster();

            glfwSwapBuffers(window);
        }
    }

    private void run() {
        try {
            init();
            loop();

            errCallback.free();
            keyCallback.free();
            fbCallback.free();
            cpCallback.free();
            mbCallback.free();
            glfwDestroyWindow(window);
        } catch (Throwable t) {
            t.printStackTrace();
        } finally {
            glfwTerminate();
        }
    }

    public static void main(String[] args) {
        new PhotonMappingBindlessDemo().run();
    }

}